diff --git a/.github/workflows/capsule-customer-execution.yml b/.github/workflows/capsule-customer-execution.yml index 869533f2..53a41c25 100644 --- a/.github/workflows/capsule-customer-execution.yml +++ b/.github/workflows/capsule-customer-execution.yml @@ -330,6 +330,13 @@ jobs: detail = test_failure_details.get((nodeid, outcome, phase), "") exception = re.search(r"^E\s+(?:subprocess\.)?(AssertionError|FileNotFoundError|PermissionError|CalledProcessError|RuntimeError|ImportError|ModuleNotFoundError|TypeError|ValueError)(?=:|\s|$)", detail, re.MULTILINE) failure = {"test_failure_class": exception.group(1)} if exception else {} + native_skips = { + "Set SPECFACT_HATCH_PROOF_WORKSPACE with native Hatch and Python 3.11 prerequisites for the acceptance cell", + "Native Hatch and project Python are required for customer storage acceptance", + "Native Hatch and project Python are required for absolute declaration acceptance", + } + if outcome == "skipped" and detail in native_skips: + failure["test_diagnostic"] = "native_prerequisites_not_supplied" for option in ("-I", "-E", "-S"): if "project_python_option_unsupported:" + option + ";" in detail: failure.update(test_diagnostic="project_python_option_unsupported", python_option=option) @@ -382,6 +389,8 @@ jobs: ): location = {"file": file, "line": line, "severity": severity} public_values = { + "differential_state": {"introduced", "fixed", "unchanged", "unknown"}, + "status": {"open", "fixed", "waived-by-reference"}, "category": {"clean_code", "security", "type_safety", "contracts", "testing", "style", "architecture", "tool_error", "naming", "kiss", "yagni", "dry", "solid", "ai_bloat"}, "tool": {"ruff", "radon", "radon-kiss", "semgrep", "ast", "basedpyright", "pylint", "contract_runner", "crosshair", "pytest"}, "rule": {"tool_error", "TEST_OUTCOME_NOT_PASS", "TEST_COVERAGE_POLICY_FAILED", "MISSING_ICONTRACT", "CROSSHAIR_COUNTEREXAMPLE", "CROSSHAIR_INCOMPLETE_EVIDENCE", "kiss.loc.error", "kiss.loc.warning", "kiss.nesting.error", "kiss.nesting.warning", "kiss.parameter-count.error", "kiss.parameter-count.warning", *(f"CC{number}" for number in range(1, 1001))}, @@ -390,6 +399,8 @@ jobs: value = finding.get(key) if isinstance(value, str) and value in allowed: location[key] = value + if type(finding.get("blocking")) is bool: + location["blocking"] = finding["blocking"] if location.get("tool") == "pytest" and location.get("rule") == "TEST_OUTCOME_NOT_PASS": location.update(_public_test_observation(finding, file)) if location.get("category") == "tool_error": @@ -401,7 +412,21 @@ jobs: ("semgrep returned empty stdout", "empty_output"), ("unrecognized crosshair output", "unrecognized_output"), ) + if location.get("tool") == "pytest": + location["diagnostic_code"] = "unclassified" if isinstance(message, str) and location.get("tool") == "pytest": + diagnostic_message = message + producer_prefixes = { + "Unable to execute targeted tests: ": "pytest_execute_error", + "Unable to read targeted pytest evidence: ": "pytest_evidence_read_error", + } + for prefix, failure_class in producer_prefixes.items(): + if message.startswith(prefix): + location["failure_class"] = failure_class + diagnostic_message = message[len(prefix):] + break + if message == "Targeted pytest observer, JUnit, and process outcomes could not be reconciled.": + location["failure_class"] = "pytest_outcome_reconciliation_failed" pytest_codes = { "project_pytest_request_invalid", "project_pytest_configuration_or_collection_failed", "project_pytest_collection_error", "project_pytest_root_missing_or_invalid", @@ -418,14 +443,14 @@ jobs: "project_pytest_installed_coverage_unavailable", "project_pytest_installed_coverage", "project_pytest_failure_without_observed_test", "project_pytest_internal_error", } - prefix = message.split(":", 1)[0].split(";", 1)[0] + prefix = diagnostic_message.split(":", 1)[0].split(";", 1)[0] if prefix in pytest_codes: location["diagnostic_code"] = prefix - elif message.startswith("[Errno 2] "): + elif diagnostic_message.startswith("[Errno 2] "): location["diagnostic_code"] = "file_missing" - elif message.startswith("[Errno 13] "): + elif diagnostic_message.startswith("[Errno 13] "): location["diagnostic_code"] = "permission_denied" - if isinstance(message, str): + if isinstance(message, str) and "failure_class" not in location: for marker, classification in classes: if marker in message.lower(): location["failure_class"] = classification @@ -475,6 +500,8 @@ jobs: continue incomplete_sides = [side for side in ("base", "head") if isinstance(row.get(side), dict) and (row[side].get("execution_state") == "error" or row[side].get("evidence_outcome") == "UNKNOWN")] + if not incomplete_sides and (row.get("execution_state") == "error" or row.get("evidence_outcome") == "UNKNOWN"): + incomplete_sides = ["unavailable"] if incomplete_sides: print(json.dumps({"analyzer": row["id"], "incomplete_snapshot_sides": incomplete_sides})) tokens = str(row.get("diagnostic", "")).split(":")[:3] @@ -894,6 +921,13 @@ jobs: detail = test_failure_details.get((nodeid, outcome, phase), "") exception = re.search(r"^E\s+(?:subprocess\.)?(AssertionError|FileNotFoundError|PermissionError|CalledProcessError|RuntimeError|ImportError|ModuleNotFoundError|TypeError|ValueError)(?=:|\s|$)", detail, re.MULTILINE) failure = {"test_failure_class": exception.group(1)} if exception else {} + native_skips = { + "Set SPECFACT_HATCH_PROOF_WORKSPACE with native Hatch and Python 3.11 prerequisites for the acceptance cell", + "Native Hatch and project Python are required for customer storage acceptance", + "Native Hatch and project Python are required for absolute declaration acceptance", + } + if outcome == "skipped" and detail in native_skips: + failure["test_diagnostic"] = "native_prerequisites_not_supplied" for option in ("-I", "-E", "-S"): if "project_python_option_unsupported:" + option + ";" in detail: failure.update(test_diagnostic="project_python_option_unsupported", python_option=option) @@ -946,6 +980,8 @@ jobs: ): location = {"file": file, "line": line, "severity": severity} public_values = { + "differential_state": {"introduced", "fixed", "unchanged", "unknown"}, + "status": {"open", "fixed", "waived-by-reference"}, "category": {"clean_code", "security", "type_safety", "contracts", "testing", "style", "architecture", "tool_error", "naming", "kiss", "yagni", "dry", "solid", "ai_bloat"}, "tool": {"ruff", "radon", "radon-kiss", "semgrep", "ast", "basedpyright", "pylint", "contract_runner", "crosshair", "pytest"}, "rule": {"tool_error", "TEST_OUTCOME_NOT_PASS", "TEST_COVERAGE_POLICY_FAILED", "MISSING_ICONTRACT", "CROSSHAIR_COUNTEREXAMPLE", "CROSSHAIR_INCOMPLETE_EVIDENCE", "kiss.loc.error", "kiss.loc.warning", "kiss.nesting.error", "kiss.nesting.warning", "kiss.parameter-count.error", "kiss.parameter-count.warning", *(f"CC{number}" for number in range(1, 1001))}, @@ -954,6 +990,8 @@ jobs: value = finding.get(key) if isinstance(value, str) and value in allowed: location[key] = value + if type(finding.get("blocking")) is bool: + location["blocking"] = finding["blocking"] if location.get("tool") == "pytest" and location.get("rule") == "TEST_OUTCOME_NOT_PASS": location.update(_public_test_observation(finding, file)) if location.get("category") == "tool_error": @@ -965,7 +1003,21 @@ jobs: ("semgrep returned empty stdout", "empty_output"), ("unrecognized crosshair output", "unrecognized_output"), ) + if location.get("tool") == "pytest": + location["diagnostic_code"] = "unclassified" if isinstance(message, str) and location.get("tool") == "pytest": + diagnostic_message = message + producer_prefixes = { + "Unable to execute targeted tests: ": "pytest_execute_error", + "Unable to read targeted pytest evidence: ": "pytest_evidence_read_error", + } + for prefix, failure_class in producer_prefixes.items(): + if message.startswith(prefix): + location["failure_class"] = failure_class + diagnostic_message = message[len(prefix):] + break + if message == "Targeted pytest observer, JUnit, and process outcomes could not be reconciled.": + location["failure_class"] = "pytest_outcome_reconciliation_failed" pytest_codes = { "project_pytest_request_invalid", "project_pytest_configuration_or_collection_failed", "project_pytest_collection_error", "project_pytest_root_missing_or_invalid", @@ -982,14 +1034,14 @@ jobs: "project_pytest_installed_coverage_unavailable", "project_pytest_installed_coverage", "project_pytest_failure_without_observed_test", "project_pytest_internal_error", } - prefix = message.split(":", 1)[0].split(";", 1)[0] + prefix = diagnostic_message.split(":", 1)[0].split(";", 1)[0] if prefix in pytest_codes: location["diagnostic_code"] = prefix - elif message.startswith("[Errno 2] "): + elif diagnostic_message.startswith("[Errno 2] "): location["diagnostic_code"] = "file_missing" - elif message.startswith("[Errno 13] "): + elif diagnostic_message.startswith("[Errno 13] "): location["diagnostic_code"] = "permission_denied" - if isinstance(message, str): + if isinstance(message, str) and "failure_class" not in location: for marker, classification in classes: if marker in message.lower(): location["failure_class"] = classification @@ -1062,6 +1114,8 @@ jobs: node = row.get(side) if isinstance(node, dict) and (node.get("execution_state") == "error" or node.get("evidence_outcome") == "UNKNOWN"): observed.setdefault(name, set()).add(side) + if name not in observed and (row.get("execution_state") == "error" or row.get("evidence_outcome") == "UNKNOWN"): + observed[name] = {"unavailable"} return {name: sorted(sides) for name, sides in sorted(observed.items())} report, header = {}, {} @@ -1123,7 +1177,8 @@ jobs: "diagnostic_observations": codes or ["unclassified"]} if outcome == "oversized_report": projected["report_header_observed"] = bool(header) + if outcome in ("structured_report", "oversized_report"): projected["error_analyzer_sides"] = incomplete_sides(members) - projected["crosshair_failure_observations"] = crosshair_failure_observations(header.get("findings", [])) + projected["crosshair_failure_observations"] = crosshair_failure_observations(evidence_source.get("findings", [])) print(json.dumps(projected)) PY_REVIEW_FAILURE diff --git a/.github/workflows/native-hatch-acceptance.yml b/.github/workflows/native-hatch-acceptance.yml new file mode 100644 index 00000000..c8d5fcb1 --- /dev/null +++ b/.github/workflows/native-hatch-acceptance.yml @@ -0,0 +1,113 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: native-hatch-acceptance + +on: + workflow_call: + +permissions: + contents: read + +jobs: + native-hatch: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout exact head + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + persist-credentials: false + - name: Set up project Python + id: project-python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: "3.11.15" + - name: Set up controller Python + id: controller-python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: "3.12.13" + - name: Install pinned native Hatch + id: hatch + shell: bash + run: | + set -euo pipefail + python -m pip install hatch==1.18.1 + echo "executable=$(command -v hatch)" >> "$GITHUB_OUTPUT" + - name: Bootstrap repository Hatch environment serially + shell: bash + run: | + set -euo pipefail + hatch run python -m pip --version + hatch run dev-deps + - name: Run mapped native Hatch acceptance + env: + SPECFACT_HATCH_PROJECT_PYTHON: ${{ steps.project-python.outputs.python-path }} + SPECFACT_HATCH_EXECUTABLE: ${{ steps.hatch.outputs.executable }} + SPECFACT_HATCH_PROOF_WORKSPACE: ${{ runner.temp }}/native-hatch-proof + NATIVE_HATCH_JUNIT: ${{ runner.temp }}/native-hatch-acceptance/junit.xml + run: | + hatch run python - <<'PY' + import os + import subprocess + import sys + import xml.etree.ElementTree as ET + from pathlib import Path + + import yaml + + mapping = Path("openspec/changes/code-review-17-project-runtime-bootstrap/requirements-evidence.yaml") + if not mapping.is_file(): + archived = list(Path("openspec/changes/archive").glob( + "*-code-review-17-project-runtime-bootstrap/requirements-evidence.yaml" + )) + if len(archived) != 1: + raise SystemExit("Native acceptance mapping needs exactly one active or archived source") + mapping = archived[0] + requirements = yaml.safe_load(mapping.read_text())["requirements"] + selectors = [ + case["selector"]["node_id"] + for requirement in requirements.values() + for case in requirement.get("verification_cases", []) + if case.get("method") == "test" and case.get("selector", {}).get("runner") == "pytest" + ] + if not selectors or any(not isinstance(item, str) or not item for item in selectors): + raise SystemExit("Native acceptance mapping must contain nonempty pytest selectors") + if len(set(selectors)) != len(selectors): + raise SystemExit("Native acceptance mapping contains duplicate selectors") + report = Path(os.environ["NATIVE_HATCH_JUNIT"]) + report.parent.mkdir(parents=True, exist_ok=True) + if report.exists(): + raise SystemExit("Native acceptance report path must be fresh") + completed = subprocess.run( + [sys.executable, "-m", "pytest", *selectors, + "-o", "addopts=-ra -q --import-mode=importlib", + "-o", "junit_family=xunit1", "--junitxml", str(report)], + check=False, + ) + if completed.returncode != 0 or not report.is_file(): + raise SystemExit("Native acceptance pytest failed or produced no report") + result = ET.parse(report).getroot() + if result.tag not in {"testsuite", "testsuites"}: + raise SystemExit("Native acceptance report has an invalid root") + if any(element.tag in {"skipped", "failure", "error"} for element in result.iter()): + raise SystemExit("Native acceptance report contains an unsuccessful outcome") + observed = [] + for case in result.iter("testcase"): + properties = case.findall("./properties/property[@name='specfact.selector']") + if len(properties) != 1 or not properties[0].get("value"): + raise SystemExit("Each testcase must report exactly one selector property") + observed.append(properties[0].get("value")) + if len(observed) != len(selectors) or set(observed) != set(selectors): + raise SystemExit("Native acceptance report does not match the exact mapped selectors") + PY + - name: Upload native Hatch acceptance evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: native-hatch-acceptance + path: | + ${{ runner.temp }}/native-hatch-acceptance/junit.xml + ${{ runner.temp }}/native-hatch-proof/receipt.json + if-no-files-found: error diff --git a/.github/workflows/native-pr-review.yml b/.github/workflows/native-pr-review.yml new file mode 100644 index 00000000..7888f934 --- /dev/null +++ b/.github/workflows/native-pr-review.yml @@ -0,0 +1,129 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: native-pr-review + +on: + workflow_call: + +permissions: + contents: read + +jobs: + independent-review: + name: independent signed native review + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Checkout exact head + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + persist-credentials: false + - name: Set up project Python + id: project-python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: "3.11.15" + - name: Set up controller Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: "3.12.13" + - name: Install independent signed reviewer + env: + EVENT_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + EVENT_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + run: | + set -euo pipefail + trusted="$RUNNER_TEMP/native-independent-review" + mkdir -m 0700 "$trusted" "$trusted/home" "$trusted/output" + python -I -m venv "$trusted/venv" + "$trusted/venv/bin/python" -I -m pip install specfact-cli==0.55.4 + base="$(git merge-base "$EVENT_HEAD" "$EVENT_BASE")" + git show "$base:pyproject.toml" > "$trusted/base-pyproject.toml" + git show "$base:packages/specfact-code-review/src/specfact_code_review/resources/contracts/pr-range-v1-toolchain-lock.json" > "$trusted/analyzer-lock.json" + "$trusted/venv/bin/python" -I - "$trusted" <<'PYTHON' + import json, sys, tomllib + from pathlib import Path + from packaging.requirements import Requirement + root = Path(sys.argv[1]) + config = tomllib.loads((root / "base-pyproject.toml").read_text()) + dependencies = config["tool"]["hatch"]["envs"]["default"]["dependencies"] + (root / "requirements.txt").write_text("\n".join(item for item in dependencies if Requirement(item).name != "semgrep") + "\n") + lock = json.loads((root / "analyzer-lock.json").read_text()) + environment = next(item for item in lock["environments"] if item["python_abi"] == "cp312" and item["os"] == "linux") + pins = environment["wheelhouse"]["direct_requirements"] + (root / "constraints.txt").write_text("\n".join(item["normalized_name"] + item["specifier"] for item in pins) + "\n") + semgrep = next(item for item in pins if item["normalized_name"] == "semgrep") + (root / "semgrep-requirement.txt").write_text("semgrep" + semgrep["specifier"] + "\n") + wheels = environment["wheelhouse"]["wheels"] + (root / "semgrep-constraints.txt").write_text("\n".join(item["normalized_name"] + "==" + item["version"] for item in wheels) + "\n") + PYTHON + "$trusted/venv/bin/python" -I -m pip install -r "$trusted/requirements.txt" -c "$trusted/constraints.txt" + python -I -m venv "$trusted/semgrep" + "$trusted/semgrep/bin/python" -I -m pip install -r "$trusted/semgrep-requirement.txt" -c "$trusted/semgrep-constraints.txt" + cd "$trusted" + env -i HOME="$trusted/home" PATH="$trusted/venv/bin:/usr/bin:/bin" SPECFACT_MODULES_BRANCH=main \ + "$trusted/venv/bin/python" -I -m specfact_cli.cli \ + module install nold-ai/specfact-code-review --scope user --version 0.51.2 --source marketplace + cp "$GITHUB_WORKSPACE/scripts/native_pr_review.py" "$trusted/native_pr_review.py" + - name: Verify immutable core registry APIs + run: | + set -euo pipefail + trusted="$RUNNER_TEMP/native-independent-review" + cd "$trusted" + env -i HOME="$trusted/home" PATH="$trusted/venv/bin:/usr/bin:/bin" \ + "$trusted/venv/bin/python" -I - <<'PYTHON' + from importlib import import_module + from importlib.metadata import version + assert version("specfact-cli") == "0.55.4" + for name, entry in ( + ("module_discovery", "discover_all_modules_for_project_with_shadowed"), + ("module_installer", "verify_module_artifact"), + ): + assert callable(getattr(import_module(f"specfact_cli.registry.{name}"), entry)) + PYTHON + - name: Bootstrap native repository environment + run: | + set -euo pipefail + python -m pip install hatch==1.18.1 + hatch run python -m pip --version + hatch run dev-deps + - name: Capture exact native subject + env: + EVENT_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + EVENT_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + run: | + set -euo pipefail + trusted="$RUNNER_TEMP/native-independent-review" + base="$(git merge-base "$EVENT_HEAD" "$EVENT_BASE")" + git -c core.hooksPath=/dev/null worktree add --detach "$trusted/subject" "$EVENT_HEAD" + git -C "$trusted/subject" -c core.hooksPath=/dev/null reset --soft "$base" + ln -s "$GITHUB_WORKSPACE/.venv" "$trusted/subject/.venv" + echo "NATIVE_REVIEW_HEAD=$EVENT_HEAD" >> "$GITHUB_ENV" + echo "NATIVE_REVIEW_BASE=$base" >> "$GITHUB_ENV" + - name: Run complete independent native review + env: + SPECFACT_HATCH_PROJECT_PYTHON: ${{ steps.project-python.outputs.python-path }} + run: | + set -euo pipefail + trusted="$RUNNER_TEMP/native-independent-review" + project_paths="$(find "$trusted/subject/packages" -mindepth 2 -maxdepth 2 -type d -name src -print | sort | paste -sd : -)" + project_paths="$trusted/subject/src:$project_paths:$trusted/subject" + cd "$trusted" + env -i HOME="$trusted/home" TMPDIR="$RUNNER_TEMP" \ + PATH="$trusted/venv/bin:$trusted/semgrep/bin:$(dirname "$SPECFACT_HATCH_PROJECT_PYTHON"):$(dirname "$(command -v hatch)"):/usr/local/bin:/usr/bin:/bin" \ + PYTHONPATH="$project_paths" SPECFACT_MODULES_BRANCH=main \ + NATIVE_REVIEW_HEAD="$NATIVE_REVIEW_HEAD" NATIVE_REVIEW_BASE="$NATIVE_REVIEW_BASE" \ + SPECFACT_HATCH_PROJECT_PYTHON="$SPECFACT_HATCH_PROJECT_PYTHON" \ + SPECFACT_HATCH_EXECUTABLE="$(command -v hatch)" \ + SPECFACT_HATCH_PROOF_WORKSPACE="$trusted/hatch-proof" \ + "$trusted/venv/bin/python" -I "$trusted/native_pr_review.py" \ + --subject "$trusted/subject" --project-python "$GITHUB_WORKSPACE/.venv/bin/python" \ + --output "$trusted/output" + - name: Upload exact-head native review evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: native-review-${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/native-independent-review/output/ + if-no-files-found: error diff --git a/.github/workflows/native-review-scope.yml b/.github/workflows/native-review-scope.yml new file mode 100644 index 00000000..50cabff8 --- /dev/null +++ b/.github/workflows/native-review-scope.yml @@ -0,0 +1,21 @@ +name: Native review migration scope + +on: + workflow_call: + +permissions: + contents: read + +jobs: + native-scope: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: "3.12" + - run: python scripts/check_native_review_scope.py diff --git a/.github/workflows/pr-orchestrator.yml b/.github/workflows/pr-orchestrator.yml index 85cb0efd..28c18279 100644 --- a/.github/workflows/pr-orchestrator.yml +++ b/.github/workflows/pr-orchestrator.yml @@ -116,17 +116,24 @@ jobs: "${VERIFY_CMD[@]}" - customer-capsules: - name: customer-capsules - needs: [changes] - if: github.event_name == 'pull_request' && needs.changes.outputs.capsule_changed == 'true' - uses: ./.github/workflows/capsule-customer-execution.yml + native-review: + uses: ./.github/workflows/native-pr-review.yml + permissions: + contents: read + + native-scope: + uses: ./.github/workflows/native-review-scope.yml + permissions: + contents: read + + native-hatch: + uses: ./.github/workflows/native-hatch-acceptance.yml permissions: contents: read quality: name: quality (${{ matrix.python-version }}) - needs: [changes, verify-module-signatures, customer-capsules] + needs: [changes, verify-module-signatures, native-review, native-scope, native-hatch] if: always() && needs.changes.outputs.code_changed == 'true' runs-on: ubuntu-latest strategy: @@ -134,20 +141,44 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - name: Require signature and customer capsule prerequisites + - name: Require signature prerequisite shell: bash env: SIGNATURE_RESULT: ${{ needs.verify-module-signatures.result }} - CAPSULE_REQUIRED: ${{ github.event_name == 'pull_request' && needs.changes.outputs.capsule_changed == 'true' }} - CAPSULE_RESULT: ${{ needs.customer-capsules.result }} run: | set -euo pipefail if [ "$SIGNATURE_RESULT" != "success" ]; then echo "Module signature prerequisite did not pass: $SIGNATURE_RESULT" >&2 exit 1 fi - if [ "$CAPSULE_REQUIRED" = "true" ] && [ "$CAPSULE_RESULT" != "success" ]; then - echo "Required customer capsule matrix did not pass: $CAPSULE_RESULT" >&2 + - name: Require independent native review + shell: bash + env: + NATIVE_REVIEW_RESULT: ${{ needs.native-review.result }} + run: | + set -euo pipefail + if [ "$NATIVE_REVIEW_RESULT" != "success" ]; then + echo "Independent native review prerequisite did not pass: $NATIVE_REVIEW_RESULT" >&2 + exit 1 + fi + - name: Require native Hatch acceptance + shell: bash + env: + NATIVE_HATCH_RESULT: ${{ needs.native-hatch.result }} + run: | + set -euo pipefail + if [ "$NATIVE_HATCH_RESULT" != "success" ]; then + echo "Native Hatch acceptance prerequisite did not pass: $NATIVE_HATCH_RESULT" >&2 + exit 1 + fi + - name: Require native migration scope + shell: bash + env: + NATIVE_SCOPE_RESULT: ${{ needs.native-scope.result }} + run: | + set -euo pipefail + if [ "$NATIVE_SCOPE_RESULT" != "success" ]; then + echo "Native review migration scope prerequisite did not pass: $NATIVE_SCOPE_RESULT" >&2 exit 1 fi - name: Skip full run (dev->main PR) @@ -158,6 +189,7 @@ jobs: uses: actions/checkout@v4 with: persist-credentials: false + fetch-depth: 0 - name: Resolve paired core CLI ref id: core-ref if: needs.changes.outputs.skip_tests_dev_to_main != 'true' @@ -206,11 +238,44 @@ jobs: - name: Install specfact-cli dependency if: needs.changes.outputs.skip_tests_dev_to_main != 'true' run: hatch run pip install -e ./specfact-cli + - name: Checkout reviewed runtime smoke fixture + if: needs.changes.outputs.skip_tests_dev_to_main != 'true' && matrix.python-version == '3.12' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + repository: nold-ai/specfact-cli-modules + ref: 046d366c190af7a188d6f48953a96edb65e766db + path: specfact-cli/runtime-smoke-modules + persist-credentials: false + - name: Verify reviewed runtime smoke fixture + if: needs.changes.outputs.skip_tests_dev_to_main != 'true' && matrix.python-version == '3.12' + shell: bash + run: | + set -euo pipefail + python -I - <<'PYTHON' + import json + from pathlib import Path + expected = { + "repository": "nold-ai/specfact-cli-modules", + "commit": "046d366c190af7a188d6f48953a96edb65e766db", + "tree": "c5b44db3477d1c9ea8dae1dceab2af276d265353", + } + actual = json.loads(Path("specfact-cli/ci/runtime-smoke-fixture.lock.json").read_text()) + if actual != expected: + raise SystemExit("Unreviewed portable runtime fixture identity") + PYTHON + fixture="$GITHUB_WORKSPACE/specfact-cli/runtime-smoke-modules" + test "$(git -C "$fixture" rev-parse HEAD)" = "046d366c190af7a188d6f48953a96edb65e766db" + test "$(git -C "$fixture" rev-parse 'HEAD^{tree}')" = "c5b44db3477d1c9ea8dae1dceab2af276d265353" + test -z "$(git -C "$fixture" status --porcelain)" - name: Runtime discovery smoke across package managers if: needs.changes.outputs.skip_tests_dev_to_main != 'true' && matrix.python-version == '3.12' run: | - python -m pip install pipx uv "virtualenv<21" - hatch run python specfact-cli/scripts/runtime_discovery_smoke.py --modules-repo "${GITHUB_WORKSPACE}" --launcher direct --launcher hatch-source --launcher pip-editable --launcher pipx --launcher uv-run --launcher uvx + launchers="$RUNNER_TEMP/runtime-smoke-launchers" + python -m venv "$launchers" + "$launchers/bin/python" -m pip install uv + (cd specfact-cli && python scripts/check_dependency_trust_exceptions.py) + UV_PROJECT_ENVIRONMENT="$RUNNER_TEMP/runtime-smoke-seed" "$launchers/bin/uv" sync --locked --all-extras --no-install-project --project specfact-cli --python "$(command -v python)" + PATH="$RUNNER_TEMP/runtime-smoke-seed/bin:$launchers/bin:$PATH" hatch run python specfact-cli/scripts/runtime_discovery_smoke.py --modules-repo "$GITHUB_WORKSPACE/specfact-cli/runtime-smoke-modules" --launcher direct --launcher hatch-source --launcher pip-editable --launcher pipx --launcher uv-run --launcher uvx - name: Validate generated command overview if: needs.changes.outputs.skip_tests_dev_to_main != 'true' run: | diff --git a/AGENTS.md b/AGENTS.md index 6092e382..e49a070b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,13 @@ This file is the mandatory bootstrap governance surface for coding agents workin ## Strategic context +The code-review execution direction is already decided: trusted local native project execution. Do not reopen capsule expansion or spend work repairing capsule prerequisites for this migration. `openspec/native-review-policy.json` owns the frozen legacy surfaces and migration baseline. + +| Rule | Enforcement | +| --- | --- | +| Preserve existing capsule history or delete it for retirement; do not add or repair classified capsule surfaces. | `scripts/check_native_review_scope.py`, invoked by mandatory `check-bundle-imports` and `.github/workflows/native-review-scope.yml`. | +| Preserve signing and independent security; failed legacy gates do not become PASS through native scope checks. | Existing signature/review gates remain unchanged; the scope checker grants no runtime or protected-review authority. | + This public modules repository does not depend on a sibling internal wiki checkout for change design. Shared design and governance context lives in the paired public `specfact-cli` repository and the active OpenSpec artifacts in this repo. When a modules change is explicitly paired with a core change, review both public change folders before widening scope or redefining shared workflow semantics. ## Canonical rule docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea042fe..8695436c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project follows SemVer for bundle versions. ## [Unreleased] +### Added + +- Prepare Codebase 0.42.0 with a trusted-project runtime service for existing + and cold detached Hatch environments. Explicit commands use the selected + project Python without synchronizing existing dependencies. Ordinary review + and legacy sidecar consumers remain on their existing routes. + ### Fixed - Prepare Code Review 0.51.4: targeted selection counts distinct Python modules, diff --git a/docs/agent-rules/50-quality-gates-and-review.md b/docs/agent-rules/50-quality-gates-and-review.md index 9fe79775..9101faef 100644 --- a/docs/agent-rules/50-quality-gates-and-review.md +++ b/docs/agent-rules/50-quality-gates-and-review.md @@ -62,8 +62,11 @@ Before running quality gates in a fresh worktree, bootstrap the Hatch environmen Run the full pipeline manually with `./scripts/pre-commit-quality-checks.sh` or `… all`. An explicitly approved local ARM64 Darwin feature-worktree exception may set -`SPECFACT_CODE_REVIEW_DEFER_TO_CI=github-linux` to defer only the capsule review -to the blocking exact-head GitHub Linux customer job. Every other hook component +`SPECFACT_CODE_REVIEW_DEFER_TO_CI=github-linux` to defer only the existing local reviewer +to the blocking exact-head GitHub Linux independent signed native review job. +The maintainer authorized this CI-only transition on 2026-10-25; its owner is +`native-pr-review.yml` with `scripts/native_pr_review.py`. Public customer +reviewer adoption remains in change 19. Every other hook component still executes. This reports DEFERRED, never PASS; it is rejected in CI, on other platforms or protected checkouts. Record maintainer approval in change evidence and require hosted review completion before merge. diff --git a/docs/guides/portable-project-runtime.md b/docs/guides/portable-project-runtime.md index e49b74a8..ec55c6a8 100644 --- a/docs/guides/portable-project-runtime.md +++ b/docs/guides/portable-project-runtime.md @@ -9,7 +9,9 @@ expertise_level: [intermediate] doc_owner: specfact-cli-modules tracks: - packages/specfact-code-review/** -last_reviewed: 2026-10-05 + - packages/specfact-codebase/src/specfact_codebase/project_runtime.py + - scripts/prove_hatch_project_runtime.py +last_reviewed: 2026-10-23 exempt: false exempt_reason: "" --- @@ -136,3 +138,72 @@ Portable analysis uses a verified private source copy with local environments an Portable reviews retain skipped, XFAIL, and XPASS outcomes as non-passing test evidence, even when other tests pass. Each reviewed production Python source needs coverage evidence of at least 80 percent, or the effective project threshold when higher. A package directory used for pytest discovery does not exempt its production files from coverage. Missing coverage remains incomplete evidence. Coverage provenance distinguishes native pytest-cov requests from review-only measurement. When the repository does not request coverage, the review collects private evidence through the supported pytest-cov lifecycle without activating its dormant aggregate `fail_under` policy. The per-reviewed-file floor above still applies. Native `--cov`, `--cov-reset`, `--cov-config`, source/include/omit settings, report destinations, threshold and precision retain their effect; actual native aggregate failures remain blocking findings. The report records both configured and effective measurement scopes. Explicit `--no-cov`, blocked coverage plugins, `--no-cov-on-fail`, excluded sources, missing distributed-worker data and unsupported plugin contracts leave required evidence incomplete. Native report write failures remain visible rather than replacing the requested output. The current lifecycle contract supports pytest-cov7.0.0 and7.1.0; xdist and configured Coverage subprocess collection use its native worker lifecycle. + +## Direct Hatch bootstrap milestone + +Codebase 0.42.0 adds a Python service for trusted local projects. This bounded +milestone supports explicitly selected detached virtual Hatch environments. +Ordinary review and existing framework sidecar commands have not migrated to it. + +```python +from pathlib import Path +from specfact_codebase.project_runtime import ( + PreparedProject, + ProjectCommand, + ProjectSelection, + inspect_project, + prepare_project, +) + +root = Path("/absolute/customer/project") +plan = inspect_project(root, ProjectSelection(environment="review")) +project = prepare_project(plan, Path("/absolute/fresh/external/invocation")) +if isinstance(project, PreparedProject): + result = project.run(ProjectCommand(("python", "-m", "pytest"), root), timeout=120) +else: + print(project.reason, project.remedy) +``` + +Existing selected environments are execution targets. Preparation never runs a +Hatch dependency synchronization against them. A cold environment requires a +compatible local Python, selected with `ProjectSelection(python="/absolute/python")` +or an unambiguous version declaration. Native Hatch creates it in the fresh +external invocation directory. Trusted native installation/build hooks may run. +This service does not provide a hostile-code sandbox. + +Bare `python` uses the probed venv executable. Other bare commands must exist in +that environment's scripts directory. A missing customer tool never falls back +to a controller executable. Commands use executable-first argv, a project working +directory and validated explicit environment overrides. Configuration/dependency +changes require reinspection. Source edits are allowed and have per-command +provenance. Normal failing tests remain normal nonzero exits. + +Named Hatch script expansion, effective native environment contexts, custom +plugins, cold locked environments, other managers and installed reviewer cutover +remain pending. Selected unsupported capabilities return actionable diagnostics. +Unused script declarations, unrelated environments and locks do not prohibit +explicit execution of a supported existing environment. + +Customer `HATCH_DATA_DIR` and `HATCH_CACHE_DIR` settings are preserved. Controller +Hatch interpreter, active-environment and target-path settings are removed. +Custom `HATCH_CONFIG` is unsupported in this slice and produces a diagnostic +before native queries, including when it changes after inspection. + +Unreadable fingerprint inputs return diagnostics or structured execution failure. +If cleanup of a failed preparation also fails, the original failure remains and +the remedy identifies the retained workspace. Use a fresh external workspace +after resolving the diagnostic. + +Run the repeatable native companion proof from a Python 3.12 controller after +providing local Hatch and Python 3.11.15. Use a fresh external workspace. + +```bash +PYTHONPATH=packages/specfact-codebase/src python scripts/prove_hatch_project_runtime.py \ + --workspace /absolute/fresh/external/proof \ + --hatch /absolute/hatch --python /absolute/python3.11 +``` + +The external `receipt.json` records actual versions, commands, inventories and +input hashes for both existing and cold cases. The labeled companion omits ODBC +only in that fixture and declares customer Ruff. The original native-dependency +reconstruction remains unchanged for later acceptance. diff --git a/openspec/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index e82b1244..54b2d616 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -38,6 +38,12 @@ The owner-approved [delivery roadmap](AGENTIC_SDLC_ROADMAP.md) adds bounded comp | 7 | Core [#682](https://github.com/nold-ai/specfact-cli/issues/682) / modules [#431](https://github.com/nold-ai/specfact-cli-modules/issues/431), core [#684](https://github.com/nold-ai/specfact-cli/issues/684) / modules [#434](https://github.com/nold-ai/specfact-cli-modules/issues/434) | Optional context binding and affected obligations. Preserve the context-free optional assurance chain; selected context binding alone waits for its contracts. Share #483 budgets. | | 8 | Modules [#169](https://github.com/nold-ai/specfact-cli-modules/issues/169) / core [#247](https://github.com/nold-ai/specfact-cli/issues/247); modules [#493](https://github.com/nold-ai/specfact-cli-modules/issues/493) | Optional Statement v1/SCAI v0.3 export and a separate exploratory pilot. No new signer/store or analytics platform; #170/#171 remain later consumers. | +## PR #524 CI transition authorization (2026-10-25) + +The maintainer explicitly authorized the native CI/security transition before merging #524 after previously deferring it. `code-review-native-pr-ci-transition` owns only required repository CI and its local Darwin deferral counterpart. Preserve the complete signed independent analyzer profile, native targeted tests/coverage, the full quality matrix, module signatures and exact-head evidence. Retire the obsolete prerequisite only through this verified replacement. Do not repair capsule runtime, tools, admission or diagnostics. + +The customer runtime delivery order remains 17 → 18 → 19 → paired core → 20. This CI exception does not adopt the public native reviewer, publish a release, complete change 17, or grant protected pr_range/core #680/C15 assurance. Source and workflow changes remain unaccepted for merge until all required native gates pass. + ## Risk-first workflow amendment (2026-10-08) Extend the existing paired `workflow-01-turn-orchestration` stories, modules [#483](https://github.com/nold-ai/specfact-cli-modules/issues/483) and core [#742](https://github.com/nold-ai/specfact-cli/issues/742), preserving the owner-approved 2026-10-04 optional-context, producer trust/basis, structured-pytest identity and shared-budget amendment. Modules owns reusable guidance/runtime; core owns wrappers/projection. Deliver skills/projection -> deterministic verification -> bounded repair -> opt-in PR automation. Skills do not activate unavailable runtime commands. The signed modules #481 -> #483 -> core #742 runtime prerequisites remain; independent projection proceeds separately. Modules #483 also retains its existing blocking relationship to core #492. diff --git a/openspec/changes/code-review-17-project-runtime-bootstrap/TDD_EVIDENCE.md b/openspec/changes/code-review-17-project-runtime-bootstrap/TDD_EVIDENCE.md new file mode 100644 index 00000000..3c6ea4ff --- /dev/null +++ b/openspec/changes/code-review-17-project-runtime-bootstrap/TDD_EVIDENCE.md @@ -0,0 +1,293 @@ +# First detached Hatch milestone evidence + +Current delivery scope, 2026-10-24: the approved result rename/decomposition and the separate existing-target, configuration and identity-traversal corrections are retained. The optional structured-attribution diagnostic experiment is withdrawn; workflow and projection tests exactly match e658e350. Earlier entries below are chronological history, including superseded experiments and failures. Final-scope results appear at the end; hosted acceptance remains blocking. + +The owner authorized this chat to take over In Progress #513 on 2026-10-23, Europe/Berlin, and implement only the first existing and cold detached Hatch milestone. The caller outcome is useful review of a trusted Python repository without injecting SpecFact dependencies or making the developer reconcile incompatible tool requirements. Full change 17, other manager adapters, ordinary reviewer cutover, paired core adoption and release acceptance remain pending. + +## Before production edits + +Base commit is `430e3ccbf9fbd6a6177b033662a76235a8bd6cc8`. The dedicated branch is `codex/feature-513-hatch-bootstrap`. Earlier archive changes are preserved in their separate worktree. Strict change-17 validation passed after the first milestone scenarios were clarified and before production code changed. + +The initial development environment selected Homebrew Python 3.14. It was recreated serially with explicitly selected Python 3.12.13, and `hatch run python -m pip --version`, `hatch run dev-deps`, SMART status and contract status completed. Setup and sandbox-permission failures are not failing-first behavior evidence. + +On 2026-10-23, before new runtime production code, the disposable regression probe `.audit/513-legacy-red.py` called the existing `create_sidecar_venv` from a Python 3.12 controller against a project declaring `.python-version = 3.11.15` and `requires-python = ">=3.11,<3.12"`. Creation succeeded, then the real created interpreter reported `[3, 12]`. The predicate expecting `[3, 11]` failed with `AssertionError: Project Python selection ignored: expected [3, 11], got [3, 12]`. + +Command was `PYTHONPATH=packages/specfact-codebase/src:packages/specfact-project/src hatch run python .audit/513-legacy-red.py`, exit 1. Output is retained in `.audit/513-legacy-red.txt`. This is an observed controller-interpreter leak in the legacy sidecar, not an import/setup failure and not a claim that its full workflow has been migrated. New-service behavior tests and their observed results must be recorded separately. + +## Implementation and acceptance + +The initial entry above predates implementation. The bounded executed evidence follows; full change-17 acceptance remains pending. The unchanged original reconstruction includes native ODBC behavior reserved for later acceptance. The first-milestone companion must identify any fixture adaptation and prove its own dependency imports, native pytest plugin, dependency or project entrypoint, customer lint configuration, controlled failure, source declarations and existing environment inventory directly. + +## Mapping sequence limitation + +The first implementation pass appended a new `first-hatch-milestone` requirement key and `tests` lists before writing tests, but did not validate them against the canonical schema/source. The new key was not a source requirement, and `tests` was not the schema-v2 executable selector shape. Production scaffolding was written before the independent reviewer identified this error. The mappings were corrected to the existing requirement keys and `selector.runner`/`selector.node_id` fields. This is a sequencing defect, not a passing original mapping gate. No historical frozen-mapping RED or owner/provider acceptance has been fabricated. + +The real legacy wrong-Python assertion still preceded any production edits. `.audit/513-service-scaffold-red.txt` records only missing-module scaffolding and is not behavior RED. Two further real regressions were observed before their respective fixes. `test_project_python_constraint_rejected_before_native_query` initially returned a ready project despite `requires-python=">=3.99"`; `test_existing_environment_discovered_without_redundant_python_override` initially refused a discoverable native existing environment because no redundant Python override was supplied. Their logs are `.audit/513-constraint-red.txt` and `.audit/513-existing-discovery-red.txt`. Both exact predicates now pass. + +## Executed first-milestone proof + +The exact selected suite command uses the repository Python 3.12.13 controller and explicit native prerequisites: + +```bash +SPECFACT_HATCH_PROOF_WORKSPACE=/private/tmp/specfact-513-native-proof-fd8c8b9 \ +SPECFACT_HATCH_EXECUTABLE=/opt/homebrew/bin/hatch \ +SPECFACT_HATCH_PROJECT_PYTHON=/Users/dom/.pyenv/versions/3.11.15/bin/python \ +.venv/bin/python -m pytest -n 0 \ + tests/unit/specfact_codebase/test_project_runtime.py \ + tests/integration/test_hatch_project_runtime.py --tb=short \ + -o junit_family=xunit1 --junitxml=.audit/513-milestone-junit.xml +``` + +The repeated suite passes all 12 selected cases. Both real targets report Python 3.11.15 and native Hatch 1.18.1. Each executes requests/pytest/pytest-asyncio imports, the dependency-provided pytest entrypoint, all eight native project tests including the asynchronous test, customer Ruff F821 enabled with F401 ignored, the disabled F821 command, and a real seeded pytest assertion failure. Existing inventory and all declaration hashes remain unchanged. Cold preparation uses the final external venv path from creation; completion metadata alone is atomically published. No SpecFact, CrossHair, beartype or icontract distribution is installed into these customer environments. + +The companion omits only its ODBC dependency/assertions, declares its own Ruff/config and pins the local proof Python. The original reconstruction remains byte-for-byte unchanged, including native assertions for later acceptance. The caller supplies the existing corpus accommodation `-p no:asyncio` to avoid duplicate plugin alias registration while leaving the configured `pytest_asyncio.plugin` and other native plugin discovery active. This is recorded argv, not hidden service behavior. Initial proof attempts exposed actual duplicate plugin startup, insufficiently verbose proof output and Hatch's selected `python` precedence; their logs remain `.audit/513-native-proof-{1,2,3}.txt`. The final native-only PATH selection honors the already probed Python and is revalidated against the actual created target. + +## Requirements evidence and source identity + +The corrected stable mapping digest is `sha256:682d94703f40635f0ace1b9ef79e8883617d02b0fcaf82d37b7452a18f81119f`. The canonical source-to-mapping check returns no findings. Genuine independent local engineering mapping acceptance is retained in `.audit/513-mapping-acceptance.json` and `.audit/513-mapping-review.md`. It is not human owner approval, protected provider provenance or runtime acceptance by itself. + +Canonical `evaluate_mapping(required_maturity="test-authored", review_evidence=...)` and `reconcile_junit(..., run_stage="final", legacy_tdd_evidence=...)` pass for the executed milestone selectors, with no findings and the explicit label `passing-after-legacy-tdd-ledger`. `.audit/513-legacy-tdd-ledger.md` and its digest-bound record retain the sequencing debt and actual failing observations. This supported transition is not frozen-mapping RED JUnit evidence for every new selector. Remaining planned inspection cases, full change delivery and protected authority remain pending. + +The first reconciliation incorrectly used the base commit as `source_ref`. It was replaced by an actual `git write-tree` object from the staged implementation, after asserting no unstaged runtime/proof/test/mapping changes. `.audit/513-tested-source.json` records that object as a local **Git tree**, separately records base ancestry, and hashes the service, proof, selectors, mapping and both fixtures. The selected suite was repeated against that exact captured source. The report does not claim that the base commit contains the new implementation or that a tree object is a commit. Later evidence-only documentation updates do not alter those executable source fingerprints. + +Codebase is version 0.42.0 for the additive API and declares controller-side `packaging` for Python constraint parsing. Canonical `sign-modules.py --allow-unsigned --payload-from-filesystem` generated a truthful checksum-only development manifest and removed the stale signature. The feature/dev verifier uses the repository public key for unchanged signed modules and passes payload/version policy. Canonical CI signing and stable publication remain pending; no private signing key was searched for and no cryptographic signing is claimed. + +Recorded log completion 513-legacy-red.txt: 2026-10-23T21:36:50+02:00. + +Recorded log completion 513-constraint-red.txt: 2026-10-23T21:50:46+02:00. + +Recorded log completion 513-existing-discovery-red.txt: 2026-10-23T21:53:24+02:00. + + +## Independent review corrections + +The independent reviewer reproduced detached-descendant pipe inheritance defeating the timeout and declared existing environments requiring an unrelated host interpreter. Parent source inspection also identified empty selected configuration bypassing detached admission. The existing mapped timeout, unsupported-context and existing-discovery cases were extended with these exact predicates. Initial local harness attempts had missing imports and do not count as behavior RED. After correcting the harness, `.audit/513-independent-review-red.txt` replays the actual pre-fix staged service against the extended tests and shows three genuine failures, including the external two-second watchdog timeout. Production fixes had been drafted before that corrected local replay; the independent reviewer's actual timeout reproduction preceded those fixes. This is explicitly a historical-source regression replay, not a claim that every local RED preceded fix authorship. + +The corrected implementation uses a selector-based bounded pipe drain, enforces detached admission even for empty tables, and probes/validates implicit existing native environments before trying any host Python discovery. The same extended predicates pass in `.audit/513-independent-review-green.txt`. Their canonical selectors and mapping digest remain unchanged. The design records the accepted internal correction and preserves the public API/scope. The final native proof also uses implicit existing discovery with declarations. + +The required manual SpecFact review was attempted with `--enforcement changed --bug-hunt --json --out .specfact/code-review.json` using repository source module roots. It exited 1. The actual schema-1.6 report is FAIL/UNKNOWN; all ten analyzer execution states are errors with `native_capsule_artifact_not_admitted:darwin-arm64-cp312`. Zero emitted findings is not successful analysis. `.audit/513-specfact-review-prerequisite-blocked.json` preserves the attempt. After the bounded fixes its source binding is stale, and the unchanged missing native artifact prerequisite remains unresolved. The identical failure is not retried, no local Darwin deferral was authorized, and this gate remains closed. Completing native first-milestone proof and other gates cannot claim merge-ready or signed release readiness. + + +The final corrected native receipt is `/private/tmp/specfact-513-native-proof-fd8c8b9/receipt.json`. The post-capture suite passes 12 selected tests in 8.77 seconds against local staged Git tree `fd8c8b9f3a659127010e34cca22d6659a27d4f18`. Recorded executable fingerprints exclude generated caches. Subsequent edits to this evidence and design are documentation-only; the tested tree remains a historical local source identity, not the unchanged base commit or protected authority. + +The parent attempted CodeRabbit review after final executable staging. Parent reports automatic approval review rejected the external diff-upload invocation because destination-specific user approval was absent. Authentication succeeded, but no CodeRabbit analysis started. This is BLOCKED external-upload approval, not a passing review or a zero-findings result. No retry or alternate upload route was used. All code remains uncommitted while the required review gates are closed. + + +A fresh independent fix verifier inspected the final executable source and native receipt and ran isolated read-only probes for empty selected admission, both declared-existing variants with host Python lookup forbidden, and detached stdout keepers with exited and live parents. It reports no remaining findings in those three corrections on POSIX. Its source/hash checks match all 21 recorded inputs and the tested `fd8c8b9...` tree. The report is `/private/tmp/specfact-513-design/fix-verification.md`, copied to `.audit/513-independent-fix-verification.md`. This bounded independent engineering verdict does not replace the blocked required reviews or certify full change 17. + +Final-source log completion `513-milestone-green.txt`: 2026-10-23T22:09:11+02:00. + +## Final quality-gate outcomes + +The refreshed executable-source gate binding is local Git tree `fd8c8b9f3a659127010e34cca22d6659a27d4f18`. All 21 recorded service/proof/test/mapping/fixture fingerprints still match both working bytes and that tree. Documentation-only updates afterward do not establish a whole-final-tree unchanged claim. Exact exit statuses, durations and log paths are `.audit/513-gate-status.json`. + +| Gate | Outcome | Evidence | +| --- | --- | --- | +| `hatch run format` | PASS, exit 0 | `.audit/513-gate-format.txt` | +| `hatch run type-check` | PASS, exit 0 | `.audit/513-gate-type-check.txt` | +| `hatch run lint` | PASS, exit 0 | `.audit/513-gate-lint.txt` | +| `hatch run yaml-lint` | PASS, exit 0 | `.audit/513-gate-yaml-lint.txt` | +| `hatch run check-bundle-imports` | PASS, exit 0 | `.audit/513-gate-check-bundle-imports.txt` | +| `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --version-check-base origin/dev --public-key-file scripts/native_release/module-signing-public.pem` | PASS, exit 0 | `.audit/513-gate-module-signature.txt` | +| `hatch run contract-test` | PASS, exit 0 | `.audit/513-gate-contract-test.txt` | +| `hatch run smart-test` | PASS, exit 0 | `.audit/513-gate-smart-test.txt` | +| `hatch run test` | PASS, exit 0 | `.audit/513-gate-test.txt` | +| `hatch run specfact code review run --enforcement changed --bug-hunt --json --out .specfact/code-review.json` | BLOCKED, exit 1, FAIL/UNKNOWN missing native capsule artifact; pre-fix source report remains stale after corrections | `.audit/513-specfact-review-prerequisite-blocked.json` | +| CodeRabbit | BLOCKED by automatic approval before external diff upload; analysis did not start | Parent-reported approval-review decision; no external findings report | + +Final SMART and full test runs each pass 5,686 tests with 72 skips, the same five baseline warnings and 95 passing subtests. Each also passes the 27 required host proofs. The native acceptance integration case is explicitly executed in the separate 12-case milestone suite rather than inferred from its prerequisite skip in ordinary full runs. Contract gate passes 28 tests. Type-check reports zero errors/warnings; lint passes. The feature/dev manifest gate verifies all seven manifests under its applicable policy; Codebase remains checksum-only awaiting canonical signing. + +The selected strict command is `openspec validate code-review-17-project-runtime-bootstrap --strict`, exit 0, retained in `.audit/513-openspec-selected.txt`. The unchanged global 37-failure strict baseline remains outside this scope. The unfinished change is not archived. Original reconstructed fixture tracked bytes match HEAD for all seven files; `.audit/513-original-fixture-integrity.json` records those comparisons. + +Confidence is high for the executed trusted POSIX first-Hatch workflow and the independently verified three corrections. Assumptions are the selected supported native Hatch environment, available compatible local Python for cold creation and trusted project-controlled execution. Full native scripts/effective-context fidelity, other managers, public/legacy consumer migration, Windows/original ODBC acceptance and signed publication remain pending. Rollback removes invocation-owned external workspaces and reverts this patch; existing customer environments are never deleted or repaired. Required review completion and merge readiness remain blocked. + + +## Authorized review corrections, 2026-10-23 Europe/Berlin + +The owner explicitly approved CodeRabbit upload and the documented Darwin exception with mandatory exact-head Linux review. The completed first CodeRabbit pass reviewed the original 22-file patch; `.audit/513-coderabbit-first.ndjson` retains its seven minor findings. Identity handling, incomplete cleanup, customer Hatch storage and watchdog robustness are accepted within the first milestone. Publication workflow changes are outside this delivery; development checksums do not establish signing or publication. The earlier blocked-upload and missing-native-capsule entries above are historical, not the current authorization state. + +Specification scenarios were updated first, then executable tests and schema-v2 mappings. Canonical planned evaluation and source-ID checks passed, all 18 selectors resolved by AST, and selected strict OpenSpec validation passed. A fresh independent reviewer accepted mapping digest `sha256:9b54f3e81c51b08737073c30a1cef8d7ab88942a9ee5ef84187f44c34a827fea` before execution or production changes. `.audit/513-review-delta-mapping-acceptance.json` and the companion review preserve this local engineering acceptance, not human approval or protected CI authority. + +The actual nine-case new regression run against unchanged production returned seven failures and two passes in 2.02 seconds, without skips. `.audit/513-review-delta-red.txt`, its JUnit XML and `513-review-delta-red-source.json` bind the observation. Inspection and pre/post-command fingerprinting raised `PermissionError: customer identity unreadable`; cleanup raised `PermissionError: workspace cleanup denied` while handling the original metadata error; custom HATCH_CONFIG produced no diagnostic; native existing custom storage incorrectly fell through to `Select a local project Python explicitly`; staged portable fixture admission failed with `requires a candidate change that schedules the blocking capsule workflow`. Unstaged fixtures and missing indexed independent review already failed closed as intended. Only after observing these failures did production correction begin. Earlier TDD sequencing debt remains unchanged and is not erased by this correctly ordered correction. + +The watchdog refinement measures only project.run, retaining its 0.2-second requested timeout and a three-second test bound against an eight-second detached descendant. Its separate outer startup/preparation watchdog is 30 seconds. This changes test robustness, not service timeout policy. + +Official Hatch configuration documentation at https://hatch.pypa.io/latest/config/hatch/ and native Hatch 1.18.1 sources were inspected by the parent on 2026-10-23. This slice preserves HATCH_DATA_DIR and HATCH_CACHE_DIR. Custom HATCH_CONFIG can change project context and remains explicitly unsupported before native queries; full manager context fidelity remains pending. + +The parent separately verified an existing base-branch hosted quality failure, tracked by https://github.com/nold-ai/specfact-cli-modules/issues/518, for smoke fixture identity mismatch against base commit 430e3cc. This change does not relax the smoke identity gate or repair that separate issue. Exact-head hosted customer review remains required before merge. + + +### Exact selector correction and final review-delta proof + +The initial review-delta AST check accepted base function selectors for parameterized tests. Canonical JUnit reconciliation requires exact node IDs, so that check was insufficient. After the initial seven-failure RED and first 21-pass GREEN, the two parameterized mapping rows were expanded to five exact selectors. Production fixes preceded this binding correction; this sequencing limitation is retained rather than described as flawless TDD. Raw earlier evidence is preserved unchanged. + +The rerunnable task-local `.audit/513-check-exact-selectors.py` collects actual pytest nodes with controlled addopts, rejects empty/duplicate required mappings and empty collections, and requires exact membership for each mapped selector. A fresh independent reviewer verified the positive 21-selector collection and actual family-selector, absent-selector and empty-mapping negative controls before accepting digest `sha256:5a1fcc479795f945916341e247a21bc579c594c2409985ecdeef5bf1629e2feb`. The original acceptance JSON is retained; its genuine `role` and `timestamp` fields are copied losslessly to canonical `reviewer_role` and `recorded_at` aliases in `.audit/513-exact-mapping-acceptance-canonical.json`. No new reviewer, timestamp or decision is invented. + +The parent then identified a live-context boundary gap. Setting HATCH_CONFIG between inspect and prepare returned PreparedProject instead of PreparationFailure. The strengthened exact mapped test genuinely failed against that production boundary in `.audit/513-live-hatch-config-red.txt` before the minimal preparation recheck was added. The final native/regression run passes all 21 exact cases without skips in 10.69 seconds. `.audit/513-review-delta-final-green.xml` and its log preserve the result. The native existing/cold receipt is `/private/tmp/specfact-513-native-proof-final-review/receipt.json`; the additional native storage case executes in the same JUnit run. + +The captured source is local staged Git tree `f8e938883f2b06dab09892882e666d3151f12082`, with 24 service, proof, test, mapping and fixture fingerprints matching the staged tree and working bytes in `.audit/513-review-delta-tested-source.json`. The source is a local tree, not a commit or protected CI authority. Canonical test-authored evaluation and final reconciliation pass with no findings using the genuine exact mapping acceptance. The combined historical basis remains `legacy-tdd-ledger`; `.audit/513-review-delta-tdd-ledger.md` records both prior sequencing debt and the actual new failures. No synthetic all-selector RED was constructed. + +Codebase remains 0.42.0 with a refreshed canonical development checksum. Publication, cryptographic signing and full change 17 remain pending. The second CodeRabbit review and required hosted exact-head review are separate from these local proofs. Subsequent prose edits do not change the 24 recorded executable inputs. + + +### Captured-stderr correction and superseding source proof + +A fresh independent correctness reviewer reproduced that both post-command fingerprint failure and configuration drift discarded native stderr while retaining stdout. The existing after-command and configuration-drift selectors were strengthened after clarifying the spec to preserve both streams with a visible bounded diagnostic. Both genuinely failed against current production before the fix; `.audit/513-stderr-preservation-red.txt` records `native-stderr` and `native-drift-stderr` missing from the returned result. The test payload remains below the native 65,536-byte stderr bound before adding the diagnostic, so it also proves that combination-induced truncation is reported. + +The internal failure-result helper prefixes the diagnostic, bounds the encoded stderr prefix before decoding, retains captured stdout, and preserves or sets output_truncated. Replacement decoding at a partial UTF-8 character can make a later UTF-8 re-encoding exceed that capture-prefix byte count; this slice does not claim a strict wire-byte budget for returned strings. Both affected paths use it. No public result shape or execution timeout changed. The exact accepted mapping digest remains `sha256:5a1fcc479795f945916341e247a21bc579c594c2409985ecdeef5bf1629e2feb`. + +The superseding selected run passes all 21 cases without skips in 10.61 seconds, recorded in `.audit/513-stderr-final-green.txt` and XML. The native receipt is `/private/tmp/specfact-513-native-proof-stderr-final/receipt.json`. Its source is local staged Git tree `3981b59bd169dbb772d93ac71c0af77106f5e1a8`; all 24 executable/test/mapping/fixture inputs match the staged tree and working bytes in `.audit/513-stderr-tested-source.json`. Canonical final reconciliation passes without findings in `.audit/513-stderr-reconciliation.json`, retaining the honest legacy transition ledger and both newly observed stderr failures. Earlier source records above remain historical rather than current. + + +### Local evidence availability + +The implementation evidence paths cited here under `.audit/` and `/private/tmp/`, including JUnit XML and native receipts, are local-only artifacts and are not included in this change. The historical execution commands above record the actual local runs. To reproduce the selected native acceptance run on another machine, set `SPECFACT_HATCH_EXECUTABLE` to an installed Hatch executable, `SPECFACT_HATCH_PROJECT_PYTHON` to the required Python 3.11 interpreter (for example, `"$HOME/.pyenv/versions/3.11.15/bin/python"`), and `SPECFACT_HATCH_PROOF_WORKSPACE` to a fresh directory path that does not yet exist, then run: + +```sh +.venv/bin/python -m pytest -n 0 tests/unit/specfact_codebase/test_project_runtime.py tests/integration/test_hatch_project_runtime.py tests/host/proof_capsule_deferred_review_ci.py::test_portable_fixture_deferral_retains_block2_guards --tb=short -o junit_family=xunit1 --junitxml=/tmp/hatch-runtime-junit.xml +``` + +This reproduction example does not replace the historical raw execution records or provide protected CI authority. + +### Second review corrections and final terminal verification + +The second external review identified disabled assertions under optimized Python and incorrect treatment of absolute Hatch Python declarations. The accepted bounded design validates proof optimization/controller prerequisites before workspace allocation, preserves absolute native declarations during selection, compares numeric version components, and validates both selected and finally created interpreter identity. It adds no public adapter or host lookup for existing numeric environments. + +The independent review accepted 32 exact collected selectors with mapping digest `d0ee81bc81481e809d2e42633f0a54faed09c0fa63f696c7698b274bba5d1533` before production changes. All eleven new cases genuinely failed against the unchanged source in `.audit/513-round2-red.xml`. Absolute-identity negative cases stopped at the earlier path-as-version rejection; their deeper mismatched-facts seams were reached only after the fix. The independent verifier also confirmed that omitting final target validation breaks the cold mismatch predicate. No historical behavior RED was replayed or fabricated. + +The first round2 formatting command failed on two test-style findings. Its shell continued to the 32-case behavioral GREEN, and the initial status update incorrectly implied formatting was complete. The fail-fast gate runner then passed type checking and failed lint before broader suites. These failures remain in `.audit/513-round2-format.txt` and `.audit/513-round2-gate-status.json`; they are not formatting acceptance. + +The third external review completed with three minor findings: mapping prose spacing, an unclear missing native-prerequisite error, and local evidence availability. The terminal changes correct those findings and the style errors. A workspace-only direct probe observed the real KeyError before the explicit named failure correction; an earlier missing-PYTHONPATH setup failure is retained separately. Unset workspace still skips the optional native cell. A configured cell missing either prerequisite now fails before allocation. Independent review verified production AST equivalence with the third reviewed artifact. + +The corrected mapping digest is `3c3efd6a4e6c945afc6e27b986deec2a814d65570c41a90b4228cb38e74458e0`. Genuine independent acceptance and canonical test-authored validation preceded fresh GREEN. Formatting has an actual exit-zero record in `.audit/513-terminal-format.txt`. The final selected run passed all 32 cases without skips in 13.14 seconds, with `.audit/513-terminal-green.xml` and `/private/tmp/specfact-513-native-proof-terminal/receipt.json`. The captured local staged tree is `031159806396b3635c36354751bbdd06f0adf4ba`; all 24 executable, mapping, test and fixture fingerprints are recorded in `.audit/513-terminal-tested-source.json`. Final canonical reconciliation passes without findings in `.audit/513-terminal-reconciliation.json`, retaining the historical TDD debt. Subsequent evidence prose is separate from those frozen executable fingerprints. + +### Declared core dependency restoration + +The final SMART run exposed local environment contamination: the earlier globally scoped `SPECFACT_CLI_REPO` override caused the unchanged bootstrap to install editable core 0.55.9 from the live public checkout. The earlier approved pipeline returned zero, but that result did not preserve the declared core 0.55.4 environment. The later SMART guard correctly failed with 5697 passing cases, 77 skips and five baseline warnings; full testing stopped. No dependency pin or guard was changed. + +The local environment was restored through the same bootstrap using a complete export of public core tag `v0.55.4`, commit `d579970565530c3fd7b98bad4de90cf874c2a99d`. Its authoritative documentation checker has SHA-256 `c84069ee3eb93a568e0ed77a579118aabfb83c64175a22f3f186cb638a880435`, identical to the previously used checker. This installs editable tagged source version 0.55.4; it is not a claim that the original wheel installation remained unchanged. The live core checkout was not modified. `.audit/513-pinned-core-source.json` and `.audit/513-pinned-core-installed.json` retain provenance. The real declared-version and registry API guard passes after restoration. + +With that pinned source configured, a fresh selected run passed all 32 cases without skips in 15.88 seconds. Its separate records are `.audit/513-pinned-green.xml` and `/private/tmp/specfact-513-native-proof-pinned/receipt.json`. The accepted mapping and all 24 source fingerprints remain unchanged. Canonical final reconciliation passes without findings in `.audit/513-pinned-reconciliation.json`. Previous terminal and failed SMART records remain historical. + +### Explicit caller base-interpreter identity correction + +On 2026-10-24, PR #524 review identified that an explicit base-Python selection could accept another installation with the same full version. The final target comparison checked environment prefix and version but omitted the requested base installation. The existing first-milestone interpreter-selection requirement covers this correction. No manager, public CLI consumer or threat-model scope changed. + +The spec scenario was added first, followed by exact existing/cold regression cases and their mapping. Independent test-design acceptance preceded execution and production changes for mapping digest `sha256:765a7abc3b97d54bf3736c3823793e7728573330cf4a9aecef77dfcfaa334929`. The unchanged runtime had SHA-256 `814f6c6c6b2afd02e1f66fe6f8960d0a7b77a04917828ef5eb4bdbb571447a89`. Both tests genuinely failed in `.audit/513-explicit-base-red.xml`, with preparation returning PreparedProject instead of PreparationFailure. They use actual Python probes and pip-free venvs, substituting only the target's base-prefix fact to represent a different same-version installation. Native manager execution is separately covered by the retained acceptance cases. + +The minimal fix adds resolved base-prefix equality to the existing final identity comparison and names the observed and selected bases in its diagnostic. Existing prefix/version checks and declaration validation remain intact. Formatting passes. All 34 exact mapped cases pass without skips in 14.51 seconds in `.audit/513-explicit-base-green.xml`. Its fresh native receipt is `/private/tmp/specfact-513-explicit-base-native-bftvtb8g/proof/receipt.json`. The previous TDD sequencing limitations remain historical and are not erased by this correctly ordered correction. + +The canonical development checksum is refreshed for the changed payload at the existing 0.42.0 feature version. The preceding CI-generated signature covered the earlier payload and cannot authenticate this correction; canonical CI signing and exact-current-head Linux review remain required. Local audit records cited here remain untracked and local-only. Refreshed required quality gates and final source identity are recorded separately before delivery. + +All final pinned-environment gates exit zero: type checking, lint/format, YAML, bundle imports, development manifest/version validation, contracts, SMART, full tests and the actual staged `pre-commit-quality-checks.sh all` pipeline. `.audit/513-pinned-gate-status.json` records each command and exit status. SMART and full each pass 5698 cases with 77 skips, five baseline warnings and 95 passing subtests; the full command also passes the 30-case host deferral harness. Native acceptance is separately exercised by the zero-skip selected run above. The actual pipeline reports capsule review **DEFERRED**, not passed, under the approved Darwin exception; blocking exact-head Linux review remains required. + +After the pipeline, core still reports 0.55.4 from the complete tagged source export. All 2338 exported regular source files match the immutable archive, and the authoritative checker hash is unchanged. The 24 frozen source fingerprints also remain unchanged. Final evidence prose was staged separately after those executable checks. The existing hosted quality baseline smoke-identity issue remains tracked as public issue #518; it is not resolved or bypassed by this milestone. Development checksum validation is not a cryptographic release signature or approval to merge into main. + +The explicit-base correction passed the refreshed serial format, types, lint, YAML, imports, development signature/version, contract, SMART and full-test gates on 2026-10-24. SMART and full each reported 5,700 passed, 77 skipped, five existing warnings and 95 passing subtests; the full command also passed 30 host proofs. The complete pre-commit pipeline exited zero with the approved Darwin capsule component explicitly DEFERRED. Independent delta review accepted the two new selectors, their genuine RED/GREEN chronology and the unchanged public contract. Hosted reviews on preceding head `82221c4055832de4534e93c2c38b9bf20d60c87b` failed with static findings, six missing-prerequisite native skips and incomplete analyzer execution. These remain blocking and are not converted to a local PASS. + +### Linux finding triage and local structural cleanup, 2026-10-24 + +The required candidate and independent installed Linux reviews executed on PR head `e96e1f039a6dcd20ad9d47fd7d46928c21d54289` and failed. Candidate review reported 98 findings, including incomplete analyzer evidence; native acceptance also skipped six cases because the sealed context does not provide the explicit Hatch/Python/proof-workspace prerequisites. Public logs do not disclose the underlying unclassified contract/pytest exceptions. These are blocking failures, not the separate #518 baseline or an accepted exception. + +Host analyzer diagnostics reproduced the new-file type, Pylint, complexity, naming and simplification findings. The independently compared cleanup preserves declared behavior through bounded inspection, output-capture and command-admission helpers, keeps partial inspection diagnostics, and retains all test function names, parametrization IDs and assertions. This is a structural refactor against existing predicates, not a new behavioral requirement or a fabricated new RED sequence. All 34 existing selectors passed with zero skips after the test/proof cleanup, using a fresh real native Hatch proof workspace. Independent review then found a double cwd resolution introduced in the draft extraction; the owner restored the original single resolved Path for admission and execution before refreshed broad verification. + +Automatic approval review rejected preparation guard extraction as a scope expansion and rejected a proposed public `ProcessResult` rename as potentially breaking callers. Neither rejected patch was applied. The review-only guard proposal copies existing predicates verbatim, and the separate result-name proposal documents `ProjectCommandResult` as a possible specific name. Both await explicit permission; no alias, suppression, new workspace admission rule or enforcement change was introduced. Host diagnostics, draft proposals and scoped independent reviews remain local under `.audit/513-static-*` and `/private/tmp/513-static-*`. This cleanup remains uncommitted while its blocked portion and hosted review findings remain unresolved. Full change 17 remains open. + +Final local verification for the pending structural cleanup passed format, types, lint, YAML, imports, development checksum/version, contract, SMART, full tests and the complete staged pre-commit pipeline. SMART and full each reported 5,700 passed, 77 skipped, five existing warnings and 95 passing subtests; full testing also passed 30 host proofs. The final fresh native run reported exactly 34 passed with zero skips. A fresh independent cwd diagnostic retargeted the caller's symlink after admission and confirmed execution reused the same resolved Path and admitted root. Its runtime source SHA-256 is `8bc78766cdf0851e087ac61938f947092fd6d6618c772d46408f94747ea610a7`. Final host analyzer output is Pylint 0, BasedPyright 0, AST simplification 0, Radon 2 (unchanged preparation complexity/span) and Semgrep 1 (public result-name rule). These remaining findings and the hosted failures keep acceptance closed; the Darwin pipeline still reports capsule review DEFERRED, never PASS. + +### Additional PR review corrections, 2026-10-24 + +An independent test-design review bound to mapping `sha256:fbd48a80d972e2fea5ea15d5b25a666d82c08d61478ca5d3f9bbf505b64c59d9` preceded production edits and RED. The mapping now has 37 executable selectors and 11 inspection cases. RED genuinely reported two failures and one matching positive-control pass against runtime SHA-256 `8bc78766cdf0851e087ac61938f947092fd6d6618c772d46408f94747ea610a7`. Wrong-patch selection incorrectly returned PreparedProject, and inherited startup execution reported optimization flag 1 plus controller values instead of clean settings. The initial shell wrapper hit zsh's read-only status variable after pytest completed; both original artifacts remain preserved. A second focused run captured actual pytest exit 1 with the same two failures before any production change. + +The minimal fixes validate all numeric caller components after the real Python probe and remove ambient PYTHON startup settings without blocking existing explicit nonidentity overrides. All three new selectors then passed; a fresh full mapped run reported 37 passed and zero skips, including real existing/cold Hatch workflows. The existing portability tests now select the actual CPython base executable, and the watchdog retains its three-second assertion while allowing a one-second child deadline. Full verification and fresh independent review for this final delta remain separate from earlier 34-case evidence and are recorded below when complete. The unchanged preparation complexity and public-name findings still await the rejected static-fix disposition; neither blocked proposal was applied. + +The final additional-review increment passed format, types, lint, YAML, imports, development checksum/version, contract, SMART, full tests and the complete staged pre-commit pipeline. SMART and full each reported 5,703 passed, 77 skipped, five existing warnings and 95 passing subtests; full testing also passed 30 host proofs. Fresh independent verification checked all 24 source bindings, 37 exact selector results, both native receipt cases, scope and comments without a new correctness finding. A new authenticated CodeRabbit CLI 0.9.1 review of the ten tracked changed files completed with zero findings. These are local results; the approved Darwin capsule component remains DEFERRED and the next committed-head Linux reviews remain mandatory. + +Final host diagnostics still report only the unapplied-proposal blockers: preparation CC35 and function span, and the public ProcessResult name. Hosted quality jobs on prior head e96e1f03 stopped at the failed customer-capsule prerequisite before running their quality suite. Separately the existing Darwin managed-uv build timed out at its unchanged 1,800-second Cargo budget. Neither failure is relabeled as #518, retried without changed inputs, or addressed by weakening gates or modifying the release workflow. The independently verified in-scope corrections can be delivered as an increment while those blockers remain explicit. No merge, full change completion, archive, release, or ordinary reviewer cutover is claimed. +### Configured native-cell prerequisite correction, 2026-10-24 + +PR review thread PRRT_kwDORVEFbs6v3HUU identified inconsistent prerequisite handling inside an explicitly configured native acceptance cell. The existing/cold workflow failed when Hatch or project Python was missing, but the storage and absolute-declaration workflows skipped. The spec scenario was added first. Nine parameterized checks then called the actual three acceptance functions with missing Hatch, missing Python, or both. Before guard edits, `.audit/513-native-cell-red.xml` recorded six failures with `Configured native cell skipped missing prerequisites` and three passing controls. After the two bounded guard corrections, `.audit/513-native-cell-green.xml` recorded nine passes. Unconfigured ordinary-suite skips remain unchanged; no CI provisioning, capsule boundary, service signature, dependency or timeout changed. + +The first wider repeat retained a real setup failure in `.audit/513-native-cell-acceptance.xml`: native Hatch selected its available Python 3.11.14 rather than the fixture's declared 3.11.15, and preparation correctly refused it. The run had 45 passes and one failure. The repeat explicitly prepended the selected `/Users/dom/.pyenv/versions/3.11.15/bin` to PATH, following the native selection precedence already documented in design.md. Its fresh receipt and `.audit/513-native-cell-final.xml` recorded 46 passes without skips. These are local development results, not protected Linux review authority. The independent guard review is `/private/tmp/513-native-cell-review.md`; its original native-failure observation remains historical alongside the later evidence. + +The exact hosted head `1ec6ca2c6395ff0396f088e57d887ef6a1a4e239` failed both required Linux reviews in run 40079812950. Candidate Python 3.12 reported ten locations: one pytest tool error, six native-test skips, preparation complexity, preparation span, and result naming. The independent installed review additionally reported four Pylint locations that coincide with local imports in the base source rather than current head lines; the public projection omits source-side attribution, so their origin remains unverified. Independent contracts and targeted-pytest evidence remain incomplete. The missing native prerequisites cannot be fixed by passing host paths across the sealed boundary or suppressing skips. The two previously rejected static refactor proposals remain unapplied pending explicit approval. No failing hosted gate is waived by this test-only correction. + +The final formatted source was repeated separately: `.audit/513-native-cell-formatted.xml` contains 46 passes with zero skips. Its canonical mapping digest is `sha256:0f374b21a8fdb24bbe1d4efe6a69d6ce85c115942b2e95a5d906286786add84e`; `/private/tmp/513-native-cell-mapping-acceptance.json` and its supplement record genuine independent post-GREEN audit rather than retroactive pre-RED acceptance. Canonical test-authored validation and final JUnit reconciliation pass without findings using the retained historical ledger. `.audit/513-native-cell-tested-source.json` binds all 24 executable, fixture and mapping hashes to local staged tree `ab68230e5a173ae11f418fa47c5013d3eeaa5695`. This tree is not a protected commit. Subsequent evidence prose is separate. CodeRabbit CLI 0.9.1 completed review of all four tracked correction files with zero findings. + +All serial local gates completed successfully in `.audit/513-native-cell-gate-status.json`: format, types, lint, YAML, imports, module integrity/version, contracts, SMART, full tests and the complete pre-commit pipeline. SMART and full each reported 5,712 passes, 77 opt-in skips, five baseline warnings and 95 passing subtests. The full command also passed 30 host proofs. Only local capsule review remains explicitly DEFERRED under the approved Darwin exception, never PASS. Strict OpenSpec validation passed. Signed delivery must retain blocking exact-head Linux review and cannot claim full change-17 completion or archive. + +### Hosted test-nesting correction, 2026-10-24 + +Both mandatory Linux reviews on signed head `5ad4537e2f49aeb3f64607af9c9110aa4b8b94f2` failed in run 40081728324. The candidate reported eleven locations and the independent installed review fifteen, including the prior unresolved findings and an added KISS nesting warning in the new prerequisite regression. The actual host Radon runner reproduced four control-flow levels from that committed source. Moving the case-to-function/arguments lookup before the assertion context removes the extra branch nesting while preserving the same three real acceptance calls and nine combinations. The same Radon runner then returned no finding for the integration file, and all nine focused cases passed. `.audit/513-native-cell-nesting-diagnostic.json` and `.audit/513-native-cell-flat-green.xml` retain that evidence. No runtime, prerequisite policy, public signature, timeout, dependency or CI boundary changed. The sealed native prerequisites, undisclosed contract/pytest analyzer errors, preparation complexity/span and result-name findings remain blocking. + +The final flattened source passed all 46 exact cases without skips, with `.audit/513-native-cell-flat-acceptance.xml` and a fresh external native receipt. Canonical final reconciliation passed without findings, and all 24 source hashes match local staged tree `fb4dd3ec83d417cbbdce3d51808c380d323a47c5`. Independent source review is `/private/tmp/513-native-cell-flat-review.md`. Host Radon and Semgrep report no finding for the integration file; these remain diagnostics rather than sealed-review authority. CodeRabbit completed the two-file correction review with zero findings. The ten serial gates all passed again in `.audit/513-native-cell-flat-gate-status.json`; SMART and full each reported 5,712 passes, 77 opt-in skips, five baseline warnings and 95 passing subtests, and full testing also passed 30 host proofs. The pipeline still explicitly reports the local capsule component as DEFERRED. Existing rejected refactor proposals remain untouched, and next-head hosted review remains mandatory. + +### Exact guard extraction approved, 2026-10-24 + +The owner explicitly answered `Approve the exact guard extraction` for `/private/tmp/specfact-513-guard-extraction.patch`, SHA256 `7531942816dd1c6ae0957fba1147e24ff8b6d5efa077d8c052a8e6da4c01049e`. Approval is bound to asynchronous question `call_b4fba96412404a98a75e620a06098636`, item 0. It covers only the two guard helpers; the result-type rename remains unapproved and unapplied. The approved patch was applied without additional checks, then formatted by the required formatter. This is a structural refactor, not a behavior change or a new failing-first claim. + +Before the move, `.audit/513-approved-guard-before.json` pinned signed head `5da104b5a737bd9315e9a2f84b8f9e9ba78cffa5`, the full module AST and exact patch digest. After the move, inlining only `_check_preparation` and `_check_selected_target` at their call sites exactly restores the original full-module AST, including statement order, predicates and diagnostics. The passing proof is `.audit/513-approved-guard-equivalence.json`. All 46 existing mapped selectors passed without skips in a fresh native run recorded in `.audit/513-approved-guard-acceptance.xml`; existing tests and mappings were unchanged. Only the affected development manifest checksum was refreshed, retaining version 0.42.0 and the established CI canonical-signature handoff. + +The exact patch reduces host Radon complexity from 35 to 25 and preparation span from 104 to 90 lines, so both findings remain open. This limited approval is not permission to complete further decomposition or rename the public result type. The latest completed exact-head Linux reviews on the preceding head failed with ten candidate locations and fourteen independent-review locations; the test-nesting finding is absent there. Native prerequisites inside the sealed context and incomplete contract/pytest evidence remain blocking. No merge, full change-17 archive, gate waiver or capsule-boundary expansion is claimed. + +The independent source/comment review at `/private/tmp/513-approved-guard-review.md` independently reconstructed the same full-module AST and accepted this scope without new findings. The equivalence harness is rerunnable with `.venv/bin/python .audit/513-approved-guard-equivalence.py`. Canonical reconciliation of the unchanged mapping and the 46 passing selectors has no findings in `.audit/513-approved-guard-reconciliation.json`; all 24 source bindings match staged local tree `f266f2ca1ca85d1c0171ef2a01822367877dc4e2`. CodeRabbit completed the three-file correction review with zero findings. All ten serial local gates passed in `.audit/513-approved-guard-gate-status.json`; SMART and full each reported 5,712 passes, 77 opt-in skips, five baseline warnings and 95 passing subtests, and the full command also passed 30 host proofs. The pipeline explicitly defers only capsule review to mandatory exact-head Linux CI, never reports it as PASS. Strict OpenSpec validation passed. Canonical module signing remains delegated to the existing CI signer after the human GPG-signed delivery. + +### Approved result name and preparation decomposition, 2026-10-24 + +The owner explicitly approved the prepared result-type rename and completion of behavior-preserving preparation decomposition. This supersedes the earlier blocked-proposal status for this bounded patch. The exact rename patch SHA256 is `af3351d162bf99605533443bab6c639fe92f30d4123ec91494af5892f30db371`. The selected Astra sketch is adapted with a frozen private Hatch query record and five literal-move helpers; it adds no admission rule, alias, dependency, timeout or ordinary-review consumer. `ProjectCommandResult` retains the existing fields and outcomes. Its intentional class-name/repr change is approved. + +The rerunnable `.audit/513-final-decomposition-equivalence.py` expands only those helpers, verifies record projections, normalizes the approved name and reconstructs the entire module AST at baseline `e658e35033538c1b9e72a59a9173fccc3daff379`. It passes after formatting. Independent source/design/comment review at `/private/tmp/513-final-decomposition-source-review.md` accepted the actual runtime SHA256 `4479b7b29e3bd6773a1fa051c85173ee3c4c474234bb13b480f9a59c94b1dee6`. Environment dictionary identity, delayed version normalization, exception boundaries, allocation ownership and cleanup order are retained. Host preparation complexity is now 10 and span 38 lines; the runtime has no host Radon or Semgrep finding. A whole projection-file diagnostic still sees CC16 in unchanged `test_actual_projector_launch_excludes_untrusted_imports`; its function AST matches the preceding committed source. No threshold or suppression changed. + +The previous exact-head Linux reviews at `e658e350` failed in run 40129928202. Candidate Python 3.12 retained the ten prior locations; the independent review retained fourteen and incomplete contracts/targeted-pytest evidence. Its normal structured failure projection omitted existing side/class metadata. A bounded diagnostic extension now emits the same existing fixed analyzer-side and CrossHair exception enums for complete structured reports. It preserves allowlists, bounds, private-message omission, verdicts and failure exits. Spec and five exact parameterized tests preceded genuine independent test-design acceptance and five actual RED failures against unchanged workflow; after the minimal three-line workflow edit, all 229 projection tests passed. Acceptance metadata received a schema-only reviewer-field correction after GREEN; the original pre-RED time and digest remain preserved, not fabricated. Independent final projection review accepted the extension at `/private/tmp/513-structured-diagnostic-source-review.md`. + +Fresh `.audit/513-final-decomposition-acceptance.xml` contains 51 exact passes and zero skips, including existing and invocation-owned cold native Hatch workflows with external Python 3.11.15 and Hatch 1.18.1. Both receipts preserve declarations and installed inventories, run eight native customer tests, honor enabled/disabled Ruff configuration and expose the seeded real failure. Original fixture hashes remain unchanged. Canonical mapping digest `sha256:2ce02ab7a69dbd8572f3a7164841dea1d126321a8e84647d82b41a94cb170017` and final JUnit reconciliation pass with the retained honest historical TDD ledger. All 26 source bindings match local staged tree `89a403eae42baf61f468c8acde9b800d3be68e86`, not protected CI authority; later evidence prose is separate. + +CodeRabbit CLI 0.9.1 completed the tracked seven-file delta review with one major version-bump suggestion. It is dismissed with concrete PR evidence: origin/dev is 0.41.16, this PR already bumps to unreleased 0.42.0, and checksum/version verification passes. Repeated corrections within this unpublished PR do not each require a new release version. The affected development checksum was refreshed; canonical signing remains with the existing CI signer. Required full gates and new exact-head hosted review remain separately pending until recorded below. No merge, gate waiver, trust-boundary expansion or full change-17 archive is authorized by these results. + +All ten serial gates passed for the approved decomposition in `.audit/513-final-decomposition-gate-status.json`: format, types, lint, YAML, imports, development module integrity/version, contracts, SMART, full tests and the full staged pipeline. SMART and full each reported 5,717 passed, 77 opt-in skips, five baseline warnings and 95 passing subtests. Full testing also passed 30 host proofs. The local capsule component remains explicitly DEFERRED to blocking exact-head Linux review. Independent evidence audit `/private/tmp/513-final-decomposition-evidence-review.md` verified all 26 source bindings, 51 exact selectors and both actual native receipts. A newly fetched reviewer thread about an existing-target workspace is assessed separately; it is not folded into the behavior-equivalence claim. + +### Existing-target workspace bypass fixed separately, 2026-10-24 + +Refreshed PR thread `PRRT_kwDORVEFbs6v7c-j` reports that explicit compatible base Python allowed fresh invocation storage beneath the actual existing Hatch target. Independent scope review `/private/tmp/513-existing-target-workspace-scope-review.md` distinguishes this existing-target bypass from the previously rejected arbitrary base-prefix guards. Prior authorization to resolve in-scope findings covers the minimal correction to external invocation storage. The approved behavior-preserving decomposition is separately signed as `4184c1ed8b470bc145889b34e17201fe4a802062`; its AST proof is not rewritten to hide this behavior change. + +The spec and three exact tests preceded independent pre-RED acceptance `/private/tmp/513-target-workspace-acceptance.json` at mapping `sha256:9d703515030d9217933b187c245cd21d5c4706cb076b3c0cc94593840c3b9d88`. Against unchanged runtime SHA256 `4479b7b29e3bd6773a1fa051c85173ee3c4c474234bb13b480f9a59c94b1dee6`, actual RED gave two failures and one passing control: direct and symlinked target workspaces returned PreparedProject instead of PreparationFailure. `.audit/513-target-workspace-red.xml` retains this evidence. The one-condition fix extends the existing selected-prefix refusal only for an existing target containing the resolved workspace. It retains the same diagnostic, runs before allocation, and leaves cold nesting and unrelated base installations admitted. + +All 28 runtime unit cases then passed. Fresh `.audit/513-target-workspace-acceptance.xml` records 54 exact passes with no skips, including original real existing/cold Hatch workflows and the new external positive control. The directory-list assertions prove no added/removed target paths; they do not overclaim arbitrary content immutability. Both native receipts again preserve declarations/inventories and exercise customer tests, entrypoints/configuration and the seeded real failure. Canonical final reconciliation passes with retained honest historical TDD evidence. All 26 source bindings match local staged tree `927ab5f7b1f17bd299c435cd9713b1c4b21ac60f`; evidence prose is separate and protected CI acceptance remains pending. Independent source/comment review `/private/tmp/513-target-workspace-source-review.md` accepted the minimal correction with no new findings. + +The final workspace correction passed all ten serial local gates in `.audit/513-target-workspace-gate-status.json`. SMART and full each reported 5,720 passed, 77 opt-in skips, five baseline warnings and 95 passing subtests; full also passed 30 host proofs. The complete pipeline passed while explicitly marking only capsule review DEFERRED under the owner-approved Darwin exception. Strict OpenSpec validation passed. Actual host Radon and Semgrep returned no findings for runtime and runtime tests. Independent evidence review `/private/tmp/513-target-workspace-evidence-review.md` verified all 54 exact selectors, 26 source bindings and both fresh native receipts. CodeRabbit completed the five-file correction review with only the already satisfied full-PR version-bump suggestion; the existing 0.41.16 to unreleased 0.42.0 change is retained. Next committed-head Linux candidate and independent reviews remain mandatory and unclaimed. + +### Supported pytest configuration provenance correction, 2026-10-24 + +Refreshed PR thread `PRRT_kwDORVEFbs6v7r_3` identifies four names recognized by existing runtime discovery but absent from this service's shared configuration fingerprint set. Independent scope/test-design acceptance `/private/tmp/513-pytest-config-acceptance.json` preceded actual RED and production modification at mapping `sha256:b9c8a7f0524e1c30596fa40d87da5a410e841c217fd069271408d4365aee0a57`. The scenario and four exact selectors drive an actual project command, change only one configuration file, and require admission refusal plus changed input/source identities. They do not claim native pytest parsing coverage. + +Actual `.audit/513-pytest-config-red.xml` records four failures: stale commands returned exited with customer output after editing pytest.toml, .pytest.toml, .pytest.ini or tox.ini. Production remained canonical `41f1560a`, runtime SHA256 `60f9e99679f83fad02f23d191b12b54effe914d50ca07dab321b1e701eaf2fe7`, during RED. Adding exactly those four existing supported filenames to `_CONFIG_NAMES` fixes the shared fingerprint without a new parser, dependency, command or reviewer adoption. All 32 runtime unit cases then passed. Independent source/comment review `/private/tmp/513-pytest-config-source-review.md` accepted this minimal correction. Earlier approved decomposition equivalence remains historical and separate. + +Fresh `.audit/513-pytest-config-acceptance.xml` records 58 exact passes with zero skips. Canonical final reconciliation passes with genuine independent acceptance and the retained honest historical TDD ledger. All 26 source bindings match staged tree `02df9b878daba97f7e1b67ffcdefed4ea92a9882`; subsequent evidence prose is separate. Both fresh native receipts preserve declarations/inventories, selected Python 3.11.15 under controller 3.12.13, customer commands/configuration and seeded-failure behavior. Full mandated gates, canonical signature handoff and next-head Linux results remain separately pending until recorded. This correction does not resolve missing signed native prerequisites or undisclosed analyzer errors by policy changes. + +### Optional diagnostic experiment withdrawn; final scope, 2026-10-24 + +Candidate Linux review on `41f1560a` completed FAIL with 117 locations after the optional diagnostic extension touched an existing projection-test file. The runtime preparation complexity/span and result-name findings are absent. Seven original native/tool-error locations remain. Newly selected legacy tests add 31 FileNotFoundError outcomes from literal bash launches, alongside existing CC16, one AST finding and 77 Pylint locations in that file. `/private/tmp/513-projection-linux-scope-trace.md` traces literal bash outside the admitted analyzer PATH and accepts exact restoration instead of changing trust or test policy. + +The optional three-line workflow extension, its five new tests/scenario/mappings and adapted output expectations are withdrawn. Both `.github/workflows/capsule-customer-execution.yml` and `tests/unit/test_capsule_review_projection.py` now exactly match e658e350. All baseline tests remain, including all shell/security tests. No gate is bypassed, skip added or existing assertion weakened. The earlier diagnostic RED/GREEN remains honest experiment history, not final implementation. Final mapping has 53 executable cases plus 11 inspections; the four pytest-config regressions retain their genuine original pre-RED acceptance, and acceptance of the narrowed mapping is post-GREEN, not retroactively pre-RED. The superseded local config gate run passed SMART, then was deliberately interrupted during full testing after source scope changed; it is not reported as a complete pass. Definitive final-scope gates and acceptance are rerun separately. + +Definitive narrowed acceptance `.audit/513-final-scope-acceptance.xml` records 53 exact passes with zero skips, mapping digest `sha256:651c76f4e9a6081530f38652147b35f78fc7f9c334fe1651568d239176b400d7`. Canonical reconciliation passes using the genuine post-GREEN narrowing acceptance and retained original RED history. All 26 source bindings match local staged tree `008f3100c4b6bdfdf8d575f69c73f5bfa9897eeb`; independent `/private/tmp/513-final-scope-evidence-review.md` verifies those bindings, exact baseline restoration and both fresh native receipts. Runtime remains the independently accepted four-filename correction. Later evidence prose is separate. + +CodeRabbit completed review of all nine tracked final-delta files with one major request to restore structured diagnostics. That request is dismissed because the optional scenario and implementation were explicitly withdrawn after independent scope review, while all baseline tests and gates remain unchanged. The cited earlier evidence entry is chronological experiment history; the top current-scope paragraph and final withdrawal entry explicitly identify current delivery. No remaining CodeRabbit finding requests a change within the selected final scope. Earlier signature/version suggestions remain satisfied by the full-PR 0.41.16 to 0.42.0 bump and the mandatory canonical-signing handoff. + +All ten definitive final-scope gates passed in `.audit/513-final-scope-gate-status.json`: format, types, lint, YAML, imports, development integrity/version, contracts, SMART, full tests and complete staged pipeline. SMART and full each reported 5,719 passed, 77 opt-in skips, five baseline warnings and 95 passing subtests; full also passed 30 host proofs. The count removes only the five withdrawn experimental cases. Local capsule review remains explicitly DEFERRED under the approved Darwin exception. Strict OpenSpec validation passes, and protected final-head Linux review remains mandatory. The superseded independent review on 41f1560a ended with analysis_timeout and no report; its budget was not changed. No final source claim uses that failed experiment as acceptance. + +### Directory traversal and classification correction, 2026-10-24 + +PR thread `PRRT_kwDORVEFbs6v707M` identifies silently omitted unreadable directories. The original three independently accepted cases produced genuine RED against signed `20eb7633`: inspection accepted an incomplete identity; execution phases lost the traversal cause behind generic input-drift errors. The first commentary described normal execution too broadly; these precise observed outcomes supersede that description. An os.walk onerror callback then passed all 35 runtime unit cases and 56 native mapped cases, but CodeRabbit found a separate classification suppression. Local primary CPython 3.11.15/3.12.13 source confirms that DirEntry.is_dir OSError is swallowed without invoking onerror. The callback candidate remained uncommitted and its later full gate run was deliberately interrupted, not reported as a complete pass. + +Independent architecture and six-case test-design acceptance preceded the classification correction. The first iterator adapter produced TypeError and is preserved separately as `.audit/513-strict-traversal-red-harness-error.xml`; it is not production RED. After independent protocol review and adapter repair, actual `.audit/513-strict-traversal-red.xml` reported three classification failures and three passing enumeration controls before production changed. The minimal iterative scandir walker then passed all 38 runtime unit cases. It propagates both error classes through existing structured handlers, retains pruning/order/symlink policy, closes each directory handle before yielding and avoids recursive depth growth. Independent source/comment review `/private/tmp/513-strict-traversal-source-review.md` accepted it. The rerunnable `/private/tmp/513-strict-traversal-equivalence.py` compares exact ordered paths and both fingerprints against signed `20eb7633`: the 11-file link/pruning fixture and 3323-file repository matched. This is readable-tree behavior evidence, not whole-module AST equivalence after intentional bug fixes. + +The local full-acceptance count assertion caught a mapping edit that had removed a requirement header. It stopped before any 59-case native run. The mapping was reconstructed from immutable signed `20eb7633`, and independent comparison proved every original requirement, metadata item and case unchanged, with exactly six added traversal cases. Final mapping acceptance `/private/tmp/513-strict-traversal-final-mapping-acceptance.json` is explicitly post-RED/GREEN correction, not retroactive pre-RED. Original acceptance and valid RED artifacts remain historical. The final digest is `sha256:576e55876d5226416336a54202f007fea51ee5de247831b045a30fc1bb7cacb5`. + +Fresh `.audit/513-strict-traversal-acceptance.xml` records 59 exact passes with zero skips; canonical final reconciliation has no findings. All 26 source bindings match staged local tree `2b407d3f37e87ecee658eda88374b88c1a4604d9`, with later evidence prose separate. Independent `/private/tmp/513-strict-traversal-evidence-review.md` verifies those bindings, the exact restored baseline workflow/tests and both actual native receipts. Customer configuration, commands, inventories, declarations and original fixture hashes remain supported by the real proof. The completed CodeRabbit strict-traversal delta review has zero findings. All remaining local gates and next committed-head Linux acceptance are recorded separately below when complete. + +All ten final strict-traversal local gates passed in `.audit/513-strict-traversal-gate-status.json`: format, types, lint, YAML, imports, development module integrity/version, contracts, SMART, full tests and complete staged pipeline. SMART and full each reported 5,725 passed, 77 opt-in skips, five baseline warnings and 95 passing subtests; full also passed 30 host proofs. The final pipeline validates the corrected staged mapping, and strict OpenSpec validation passes. The capsule component remains explicitly DEFERRED under the approved Darwin exception. Next committed-head candidate/independent Linux review remains mandatory; no existing native prerequisite, analyzer failure or timeout is waived. + +## PR524 coverage configuration review correction + +Review comment 4372310790 identified omitted .coveragerc provenance. A new focused test first executed the customer command after changing coverage selection, reproducing unchanged accepted inputs. Adding .coveragerc to the existing configuration-name inventory now returns execution_failed before command output and changes both source and input identities. Existing traversal, manager selection and admission behavior remain unchanged. The focused runtime/CI run passed 73 cases after this correction. The required native mapping includes the new case; module checksum/signature refresh and exact-head Linux verification remain required. diff --git a/openspec/changes/code-review-17-project-runtime-bootstrap/design.md b/openspec/changes/code-review-17-project-runtime-bootstrap/design.md index e033d5cc..c3b2d289 100644 --- a/openspec/changes/code-review-17-project-runtime-bootstrap/design.md +++ b/openspec/changes/code-review-17-project-runtime-bootstrap/design.md @@ -39,39 +39,41 @@ The public boundary hides manager orchestration and subprocess mechanics. Its sk ```python @dataclass(frozen=True) class ProjectSelection: - python: str | None - manager: str | None - environment: str | None - groups: tuple[str, ...] - extras: tuple[str, ...] + python: str | None = None + manager: str | None = None + environment: str | None = None + groups: tuple[str, ...] = () + extras: tuple[str, ...] = () + hatch: str = "hatch" @dataclass(frozen=True) class ProjectCommand: argv: tuple[str, ...] cwd: Path - env_overrides: Mapping[str, str] + env_overrides: Mapping[str, str] = field(default_factory=dict) @dataclass(frozen=True) class ProjectPlan: root: Path selection: ProjectSelection - dependency_inputs: tuple[Path, ...] - configuration_inputs: tuple[Path, ...] - source_identity: SourceIdentity + input_identity: str + environment: str | None + python_request: str | None + python_constraint: str + diagnostics: tuple[str, ...] class PreparedProject: - root: Path - python: Path - environment_identity: EnvironmentIdentity + plan: ProjectPlan + identity: EnvironmentIdentity - def run(self, command: ProjectCommand, *, timeout: float) -> ProcessResult: + def run(self, command: ProjectCommand, *, timeout: float) -> ProjectCommandResult: raise NotImplementedError -def inspect_project(root: Path, selection: ProjectSelection) -> ProjectPlan: +def inspect_project(root: Path, selection: ProjectSelection | None = None) -> ProjectPlan: raise NotImplementedError @@ -154,7 +156,27 @@ Laziness Protocol determines the single runner and small service. Subtract Befor ## Delivery sequence and reconciliation -Changes 17, 18, 19, paired core adoption, then 20 form the runtime delivery sequence. All current implementation tasks remain unchecked. During implementation, bind scenario-derived tests into the requirement mappings with executable selectors before RED and require the Requirements evidence gate to verify actual results at implementation maturity before GREEN; planning inspection is insufficient. Change 17 proves one complete existing/cold Hatch workflow before expanding managers. Change 18 establishes project standards and analyzer fidelity. Change 19 emits local evidence and prepares the single-runner cutover candidate, deleting both obsolete execution routes in that candidate. Core adopts ordinary consumption against the exact reviewed immutable signed candidate, without requiring its stable publication first. Change 20 proves installations, replaces capsule-only CI and owns coordinated stable canonical publication after paired compatibility and release acceptance. Candidate preparation does not expose an unaccepted stable cutover or add a reverse implementation dependency. +### Authorized first Hatch milestone, 2026-10-23 + +The owner explicitly authorized this chat to take over In Progress #513 and deliver its first existing and cold detached Hatch milestone. The confirmed intent is straightforward trusted-project review without developer-managed conflicts between SpecFact and customer dependencies. This first slice calls the shared service directly. It does not expose an unaccepted public reviewer cutover or complete all of change 17. + +The refreshed architect comparison considered a direct prepared-environment capability and a structurally distinct external native-Hatch bridge for script/context evaluation. The independent cross-judge selected the direct capability. The three configured sketch roles were `gpt-6-astra`, `gpt-6.1-sol` and `gpt-6-luna`. These are requested dispatch roles, not independently measured model identities. The bridge would add a private-API protocol and unproved plugin/lifecycle side effects before the first workflow works. Native `hatch run` is unsuitable for existing execution because it enters preparation and can synchronize dependencies. + +Start with one cohesive `specfact_codebase.project_runtime` implementation and the public types/functions already sketched above. `ProjectSelection` has useful defaults. `ProjectCommand.argv` is always executable-first, with explicit working directory and overrides. Preparation returns either a ready project or a structured failure without a usable interpreter. Both existing and cold targets use the same execution-only `PreparedProject.run`. Native Hatch may create an owned cold environment but is absent from the executor's authority. A compatible dependency console entrypoint is explicitly sufficient for this detached first-slice proof; no project console-script installation is claimed. + +Use the Astra sketch as the base and graft Sol's stale-dependency no-sync tripwire and exact missing-command behavior. Bare `python` binds to the probed environment executable, preserving its venv path rather than resolving its symlink to the base binary. Other bare commands resolve only in the environment's executable directory. A missing tool is unavailable and never falls back to the controller PATH. Remove controller Python path/activation contamination and support validated explicit environment overrides. Preserve genuine nonzero command exits, timeouts, bounded output and execution failures separately. + +Pure inspection reads declarations without importing project setup or provisioning plugins. Admit selected manager/plugin capabilities before any potentially provisioning native query. Do not refuse explicit execution merely because unrelated scripts, environments or lockfiles exist. Reject only a selected unsupported operation with a specific remedy. Full native Hatch script expansion and unsupported effective environment-context behavior remain pending cells; do not reimplement Hatch shell expansion or silently discard required context. + +The prepared binding fixes the canonical project root, selected environment and relevant configuration/dependency inputs. Ordinary source edits between commands are allowed and must appear as current command provenance, so a seeded defect reaches the actual test runner. Configuration/dependency drift requires reinspection rather than silent synchronization. Build/cache outputs must not invalidate their own commands. This is current-worktree service evidence, not immutable range or protected authority. + +Cold preparation uses a private external invocation workspace and native Hatch with the selected project Python. Keep the environment at its final path from creation onward because moving a venv can break entrypoint shebangs. Atomically publish completion metadata only after successful preparation and identity checks. Interrupted output is not reusable readiness; clean up only owned state. Do not add a cross-invocation registry, broker or shared cache protocol. + +Preserve the original reconstructed fixture byte-for-byte. Add a labeled companion retaining detached review/default dependency separation and pytest plugin/import-layout conditions, omitting ODBC only for this first-slice companion and declaring its own customer linter/configuration. The original native-dependency assertions remain for later acceptance. Both existing and cold proofs must use actual target Python 3.11 from the Python 3.12 controller, dependency imports, a dependency entrypoint, configured native pytest plugin, enabled/disabled customer lint rules, seeded failing test, and before/after declaration and existing-environment inventory checks. Record actual native Hatch executable/version. Native Python/library or cross-platform support beyond the executed cell is not inferred. + +Model the Domain drove the ready-versus-failure and execution-only capability shape. Separate Before Serializing Shared State drove per-invocation ownership. Prove It Works drove the actual wrong-interpreter RED and complete two-case workflow instead of treating import failure or planning validation as acceptance. Sketches, rubric and judgment are retained under `/private/tmp/specfact-513-design/` during this run, with the accepted synthesis recorded here. + +Changes 17, 18, 19, paired core adoption, then 20 form the runtime delivery sequence. The full-change checklist remains pending beyond the bounded first-Hatch checkpoint. During implementation, bind scenario-derived tests into the requirement mappings with executable selectors before RED and require the Requirements evidence gate to verify actual results at implementation maturity before GREEN; planning inspection is insufficient. Change 17 proves one complete existing/cold Hatch workflow before expanding managers. Change 18 establishes project standards and analyzer fidelity. Change 19 emits local evidence and prepares the single-runner cutover candidate, deleting both obsolete execution routes in that candidate. Core adopts ordinary consumption against the exact reviewed immutable signed candidate, without requiring its stable publication first. Change 20 proves installations, replaces capsule-only CI and owns coordinated stable canonical publication after paired compatibility and release acceptance. Candidate preparation does not expose an unaccepted stable cutover or add a reverse implementation dependency. At cutover, delete native macOS broker/worker and Linux OCI/Bubblewrap acquisition, capsule admission/descriptors/catalogs, sealed import policing, dependency/native-library copying and the parallel legacy host orchestrator after useful helpers move. No permanent runtime backend switch remains. Distribution cleanup removes residual native builds and capsule-only release/support configuration. Preserve cryptographic module verification and independent security work. @@ -189,3 +211,80 @@ Confidence is Medium. Ownership, observed code coupling and live issue lineage a - [uv project environment configuration](https://docs.astral.sh/uv/concepts/projects/config/) and [locked synchronization](https://docs.astral.sh/uv/concepts/projects/sync/), accessed 2026-10-16. - [Hatch virtual environment options](https://hatch.pypa.io/latest/plugins/environment/virtual/) and [Poetry configuration](https://python-poetry.org/docs/configuration/), accessed 2026-10-16. - [BasedPyright target-interpreter option](https://docs.basedpyright.com/latest/configuration/command-line/) and [CrossHair contracts](https://crosshair.readthedocs.io/en/latest/contracts.html), accessed 2026-10-16. + +### First-milestone implementation reconciliation, 2026-10-23 + +The implemented boundary retains the accepted direct execution capability. Useful defaults are concrete, and `ProjectSelection.hatch` optionally names the external native executable for reproducible local discovery. Without an explicit Python override, the selected existing native environment supplies its own interpreter, including when a valid version declaration is present; that interpreter is checked directly against the declaration. Explicit Python and project `requires-python` remain validated. The Codebase manifest declares `packaging` for the controller-side constraint parser, without installing it into the customer environment. + +`ProjectPlan.input_identity` frames declaration paths and file digests. `ProjectCommandResult` carries `outcome`, optional `exit_code`, bounded `stdout`/`stderr`, `output_truncated` and observational `source_identity`. Normal nonzero exits remain `exited`; missing tools are `unavailable`; timeouts and execution failures are distinct. The service uses Unix process groups for this executed Darwin cell. Cross-platform process control remains unproved. + +The unchanged reconstruction explicitly loads `pytest_asyncio.plugin` and also exposes its duplicate auto-discovery alias. The companion proof uses the existing corpus's visible `-p no:asyncio` accommodation, preserving explicit plugin execution and other native plugin discovery. This is a caller argv fact, not a claim that the reconstructed configuration needs no accommodation. + +Native Hatch 1.18.1 gives selected environment `python` precedence over `HATCH_PYTHON`. Cold preparation therefore supplies the explicitly probed interpreter's scripts directory first in the manager subprocess PATH as well as the native Python variable. It probes and validates the actual completed environment, preserving the stable venv path from creation. This adaptation follows the approved selection contract; it does not import private manager APIs or claim native script/context fidelity. + +### Accepted bounded review corrections, 2026-10-23 + +The parent and independent final engineering reviewer accepted three internal corrections under the existing API and first-slice scope. The review is retained at `/private/tmp/specfact-513-design/final-review.md`; parent steering in this implementation session authorizes these focused fixes. An empty selected environment configuration must still satisfy detached admission before native queries. Implicit existing environment discovery now finds/probes the native target and validates declared constraints directly, without requiring a separate host interpreter or enforcing an unrelated host patch version. Only explicit Python overrides are resolved before existing lookup; cold preparation still resolves a compatible local base interpreter. + +The first reader-thread implementation could exceed its timeout when a detached descendant retained inherited pipes, because buffered stream closure waited on a blocked reader. The accepted replacement uses POSIX selectors and nonblocking descriptors with an explicit deadline and bounded buffers. It preserves public `ProjectCommandResult` outcomes and original-process-group cleanup. It does not promise to kill independently detached descendants or provide a security sandbox. The regression measures the command independently with a three-second bound and a separate 30-second startup/preparation watchdog, and explicitly cleans up its own detached descendant. No broker, manager adapter or extra public command mode was introduced. + +The native proof now exercises implicit existing discovery with declarations and explicit cold Python selection. Fixture materialization and proof fingerprints exclude generated caches. Eight optional narration blocks were removed from the new companion/tests following the accepted scoped comment review, while original-fixture bytes, public contracts and intentional customer lint/plugin inputs remain unchanged. + + +### Bounded external-review corrections, 2026-10-23 + +The approved CodeRabbit pass identified read failures escaping structured results, cleanup masking the original preparation failure and ambient customer Hatch storage being removed. The service now reports fingerprint failures at inspection/execution boundaries without inventing a verified identity; after-command failures preserve captured output. Failed removal of incomplete owned storage preserves the primary error and identifies cleanup residue in the remedy. No source cache was added without a measured need. + +Native Hatch 1.18.1 and its official configuration documentation establish HATCH_DATA_DIR and HATCH_CACHE_DIR as storage selections. Those two values survive controller isolation; interpreter, active-environment, virtual-target and project selectors do not. A custom HATCH_CONFIG can change project context and remains unsupported in this milestone, checked during inspection and again before preparation's native queries. This preserves the requested root without introducing a global-context adapter. The real native existing-environment acceptance uses custom data/cache storage with controller contamination present. + +The user-approved Darwin delivery exception now recognizes the portable-runtime fixture path already selected by CI. Only the existing staged candidate pathspec changes. Required indexed independent review, platform/worktree/branch/CI checks and all other Block 2 checks remain in force. DEFERRED is not PASS, and hosted exact-head completion remains mandatory. + +### Linux static-finding reconciliation, 2026-10-24 + +The three architect candidates and independent cross-judge selected bounded function-oriented decomposition in the existing runtime module. The owner accepts Astra's preservation of partially inspected fields after late read failures, process deadline semantics, and single preparation cleanup owner. Sol's smaller helper discipline is retained. Internal `_environment_config` metadata becomes `hatch_environment` with the same Mapping interface, position, default and hidden representation; it has no external adopters and is absent the agreed public usage sketch. No new Plan lifecycle methods, runtime state framework or public caller options are introduced. Small inspection/native lookup value records may group facts where necessary to avoid repeated parameter bundles. + +Declaration policy, command admission, bounded output capture, native target lookup and atomic receipt publication remain behind the same inspect/prepare/run boundary. Existing diagnostic wording, order, interpreter selection, process outcomes, cleanup timing and the explicit selected-base identity guard are preserved. Unrequested base-prefix workspace-admission guards suggested by two candidates are rejected because they change behavior. Proof/test helpers retain selector IDs, conditions, assertions and fixture bytes. Static budgets are verified from actual analyzer output; no new suppression or changed enforcement is authorized. Sealed capsule missing native prerequisites and unclassified analyzer failures remain separate blockers. Candidate and cross-judge artifacts are retained under `/private/tmp/513-static-design/`. + +### Additional in-scope PR correctness findings, 2026-10-24 + +Independent pre-RED review accepted exact regressions for complete numeric interpreter selection and controller Python startup isolation after narrowing a mapping overclaim to selection rather than execution. A matching full version is a positive control. The two intended failures were reproduced against unchanged production before fixes. Numeric caller selection now validates every supplied component against the actual probe before native queries. Ambient `PYTHON*` startup settings are removed at the subprocess boundary, while existing explicit nonidentity command overrides remain available. This adds one private validation helper and no public option or changed result type. + +The test portability correction uses CPython's actual base executable, the same input used by its installed venv builder, instead of assuming an unversioned base/bin/python path. The detached-descendant watchdog allows one second for the legitimate child while retaining its independent three-second completion assertion and eight-second escaped descendant. No observed flake or speedup is claimed. Configurable cold creation budgets are deferred under open #513 because the approved first milestone retains its bounded existing/cold fixture and established timeout. No new preparation option, timeout relaxation, ordinary review adoption or capsule-boundary change is introduced. + +### Approved result naming and completed preparation structure, 2026-10-24 + +The owner explicitly approved the prepared result-type rename and completing behavior-preserving preparation decomposition. `ProcessResult` becomes `ProjectCommandResult`, with the same dataclass fields and outcome values; all current references migrate together, without a compatibility alias. Type names and their representations change as approved. Serialized field content and command behavior stay the same. + +Reuse the previously judged Astra design, with Sol's limit on unnecessary wrappers. A frozen `_HatchTarget` groups native lookup facts: executable, the unchanged version-query result, selected path, sanitized environment dictionary, and existing-target flag. The dictionary remains mutable; this value has no lifecycle methods. Keeping the version-query result defers reading and stripping its stdout until the same identity-construction point as before. + +The selected internal signatures are `_find_hatch_target(plan) -> _HatchTarget`, `_create_cold_target(plan, native, workspace) -> Path`, `_check_prepared_identity(facts, requested, target) -> None`, `_publish_completion(plan, identity, hatch, workspace) -> None`, and `_preparation_failure(plan, workspace, owned, error) -> PreparationFailure`. Bodies move the existing statements. `prepare_project` retains the explicit-Python early prefix check, implicit-existing resolution, both selected-target checks, workspace allocation and the sole ownership flag. It calls failure cleanup through one helper under the same exception boundary. Allocation sets ownership only after success, cold locked/path refusal stays after allocation, and completion publication remains after final interpreter and input validation. + +Unchanged code leaves the measured complexity and size failures; a lifecycle/session object would add partial-state invariants. The selected value and helpers give each existing operation a bounded implementation in the same module. No new admission rules, dependencies, public options, timeouts, manager support, descriptor semantics or security exceptions are introduced. An expansion harness must normalize only the approved result name and internal value projections, inline the moved bodies, and recover the preceding module AST. Existing exact acceptance and required gates remain mandatory. + +### Existing-target workspace bypass correction, 2026-10-24 + +Separate from the approved behavior-preserving decomposition, refreshed PR review identified that explicit compatible base selection bypassed the existing interpreter-prefix storage exclusion: a workspace inside the selected existing Hatch target could receive completed.json. Independent scope review accepted this as a correction to the existing separation of SpecFact invocation storage from customer execution targets under the owner's in-scope-finding authorization. It does not impose the previously rejected arbitrary base-prefix policy. + +The spec and three exact regression cases preceded independent design acceptance and actual RED: direct and symlink containment incorrectly returned PreparedProject, while external preparation passed. The minimal guard extends the existing refusal with `existing and workspace.is_relative_to(target.resolve())` before allocation. It reuses the same diagnostic and existing resolution; cold environments remain intentionally nested under invocation storage, and no new public option, manager, dependency, timeout or capsule policy is introduced. Target file-list assertions prove absence of added/removed paths, not arbitrary content immutability. This behavior fix has its own RED/GREEN and source binding; the earlier whole-module equivalence proof remains bound to decomposition commit 4184c1ed. + +### Supported pytest configuration identity, 2026-10-24 + +PR review identified four pytest configuration names already recognized by the existing review discovery surface but omitted by this service's shared input/source fingerprint set: pytest.toml, .pytest.toml, .pytest.ini and tox.ini. This is an in-scope correction to the existing stale-configuration refusal and provenance contract. Reuse the existing _CONFIG_NAMES set; adding a parser, discovery layer or new pytest dependency would duplicate established ownership. + +The scenario and four exact filename cases preceded independent scope/test-design acceptance and four actual RED failures where a configuration-only change still allowed command execution. Adding only the four names makes the existing admission guard reject stale preparation and changes both identity forms. The regression deliberately drives a Python command, so it proves admission/provenance without asserting pytest parsing coverage or requiring a different customer pytest version. This correction remains separate from the earlier approved decomposition equivalence, selected-target storage fix and pending sealed Linux prerequisites. + +### Optional diagnostic extension withdrawn, 2026-10-24 + +The hosted diagnostic experiment selected an unrelated legacy projection-test file inside a sealed analyzer environment whose PATH does not admit bare bash. That review surfaced existing shell harness and style debt beyond the first runtime milestone. The scope decision is to withdraw the optional structured-attribution extension and its five newly introduced cases, restoring the workflow and projection-test file byte-for-byte to e658e350. Every baseline test and gate remains present. No shell substitution, skip, enforcement relaxation or signed-toolchain change is introduced. The original capsule prerequisites/analyzer failures remain blocking; captured diagnostics and the genuine experimental RED/GREEN sequence remain historical evidence. The final runtime mapping has 53 executable cases, retaining all original runtime cases and the seven independently accepted workspace/configuration regressions. + +### Directory traversal failures preserve identity integrity, 2026-10-24 + +PR review found that os.walk's default error suppression could omit an unreadable subtree from both fingerprints even while commands could access a known configuration path. This is a bounded correction to the existing unreadable-identity failure contract. Reuse os.walk's onerror callback to re-raise OSError into the existing inspection/preparation/execution handlers; a custom walker would duplicate traversal and pruning semantics. Ignored directories, symlink policy, public options, cleanup, output limits and manager selection stay unchanged. + +The scenario and three independently accepted exact cases preceded real RED in inspection and before/after command boundaries. The regression injects PermissionError only at the actual scandir boundary for a real configuration-bearing subtree, while retaining real preparation and command execution. Inspection must return a diagnostic and no identity/native follow-up, pre-command failure must leave the command marker absent, and post-command failure must preserve captured output. The implementation is one standard callback and its binding; no new fallback or swallowed error is introduced. + +### Strict traversal completes the unreadable-identity correction, 2026-10-24 + +The callback-only premise was disproved by primary CPython 3.11.15 and 3.12.13 source: os.walk separately suppresses DirEntry.is_dir OSError. CodeRabbit identified this remaining failure, and independent architecture review accepted the minimal private adaptation. `_files` now uses a pending-directory stack and sorted os.scandir entries, yields each directory's sorted files before its sorted descendants, retains ignored/egg-info pruning and directory-symlink exclusion, and preserves file symlinks. Directory handles close before yielding paths; there is no recursive depth limit, new manager, public option or trust exception. Enumeration and classification errors propagate to the same existing structured handlers. + +Six exact phase/failure cases refine the three initial enumeration cases. The first adapter attempt raised a test-only iterator TypeError and is excluded from RED evidence. After a reviewed iterator-protocol correction, the callback candidate gave three classification failures and three passing enumeration controls before strict traversal was implemented. The strict walker passes all 38 runtime unit cases. A rerunnable comparison against signed `20eb7633` proves exact ordered paths and both fingerprints on an adversarial readable-tree fixture and the actual 3323-file repository. This preserves ordinary traversal behavior while intentionally correcting previously suppressed errors; it is not a claim of complete AST equivalence to the initial runtime. diff --git a/openspec/changes/code-review-17-project-runtime-bootstrap/proposal.md b/openspec/changes/code-review-17-project-runtime-bootstrap/proposal.md index f1f37779..3d6e8377 100644 --- a/openspec/changes/code-review-17-project-runtime-bootstrap/proposal.md +++ b/openspec/changes/code-review-17-project-runtime-bootstrap/proposal.md @@ -30,7 +30,9 @@ Parent Feature [#163](https://github.com/nold-ai/specfact-cli-modules/issues/163 ## Impact -This delivery changes planning artifacts and GitHub alignment only. It changes no production package, test, workflow, module manifest, registry, signature, version or published capability. All implementation boxes remain unchecked and requirement mappings use inspection at planned maturity. +The merged 2026-10-20 delivery changed planning artifacts and GitHub alignment only. On 2026-10-23 the owner authorized takeover of #513 and implementation of only the first existing and cold detached Hatch milestone. The ordinary public reviewer cutover, remaining manager adapters, paired core adoption and release acceptance remain later work. Completion of this milestone does not complete or archive change 17. + +The confirmed customer outcome is straightforward review of a trusted Python repository without making its developer resolve SpecFact dependency conflicts. Existing project Python, dependencies, native plugins and configuration remain authoritative. SpecFact and analyzer packages stay outside the customer environment. Read-only inspection and actionable ambiguity/preparation failures must not substitute the controller Python or weaken project constraints. Future implementation affects the owning Codebase/Code Review modules and ordinary core consumers, with semver, signing and exact installed acceptance required. There is no hostile-code sandbox, custom environment broker, mandatory customer coverage floor, mandatory contract decoration, or protected local PR authority. @@ -45,7 +47,7 @@ Every requirement and scenario in [the delta spec](specs/project-runtime-bootstr - **Issue URL**: - **Last Synced Status**: open - OpenSpec change: `code-review-17-project-runtime-bootstrap`. -- Status: open / Todo / planned, verified 2026-10-16. +- Status: open / In Progress, verified 2026-10-23. The owner explicitly authorized this chat to take over the first Hatch milestone. - Assignee: djm81. Project: SpecFact CLI. Issue type: User Story. - User-confirmed trust: dependency installation, build hooks, imports and configured checks execute locally in trusted customer repositories. - Rollback: preserve immutable compatible released CLI/module pairs; no old production backend is retained after the cutover. diff --git a/openspec/changes/code-review-17-project-runtime-bootstrap/requirements-evidence.yaml b/openspec/changes/code-review-17-project-runtime-bootstrap/requirements-evidence.yaml index d795a2af..69c5e111 100644 --- a/openspec/changes/code-review-17-project-runtime-bootstrap/requirements-evidence.yaml +++ b/openspec/changes/code-review-17-project-runtime-bootstrap/requirements-evidence.yaml @@ -22,6 +22,149 @@ requirements: intent: 'Inspect planned scenario: Ambiguous or missing interpreter.' observable: it names the candidates or missing version and performs no installation or controller-Python fallback. Planning inspection only; runtime tests, RED/GREEN, installed acceptance and publication remain pending. + - case_id: first-hatch-selected-venv-identity-and-no-sync-even-with-stale-dependencies + scenario_id: independent-customer-interpreter-selection + method: test + intent: Bounded first Hatch milestone. selected venv identity and no sync even with stale dependencies. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_selected_venv_identity_and_no_sync_even_with_stale_dependencies + - case_id: first-hatch-missing-tool-never-falls-back-to-controller-path + scenario_id: independent-customer-interpreter-selection + method: test + intent: Bounded first Hatch milestone. missing tool never falls back to controller path. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_missing_tool_never_falls_back_to_controller_path + - case_id: first-hatch-project-python-constraint + scenario_id: independent-customer-interpreter-selection + method: test + intent: Reject selected Python incompatible with project.requires-python before native manager queries. + observable: Structured failure preserves requested constraint and does not invoke native Hatch. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_project_python_constraint_rejected_before_native_query + - case_id: first-hatch-discover-existing-without-python-override + scenario_id: independent-customer-interpreter-selection + method: test + intent: Use a probeable selected existing native environment without requiring redundant caller Python configuration. + observable: Preparation discovers and validates the selected existing interpreter without lifecycle calls. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_existing_environment_discovered_without_redundant_python_override + - case_id: first-hatch-explicit-base-python-existing + scenario_id: independent-customer-interpreter-selection + method: test + intent: Bind an explicit base Python selection to the existing target installation. + observable: Equal version with a different base installation returns a diagnostic naming both bases and no + completed capability. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_explicit_base_python_must_match_prepared_target[existing] + - case_id: first-hatch-explicit-base-python-cold + scenario_id: independent-customer-interpreter-selection + method: test + intent: Bind an explicit base Python selection to the native-created cold target installation. + observable: Equal version with a different base installation returns a diagnostic naming both bases and no + completed capability. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_explicit_base_python_must_match_prepared_target[cold] + - case_id: first-hatch-absolute-python-facts-base-prefix + scenario_id: independent-customer-interpreter-selection + method: test + intent: Require absolute Hatch declaration facts to match selected target. + observable: A mismatched base prefix or full version returns structured failure naming the declaration. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_absolute_hatch_python_must_match_selected_interpreter[base-prefix] + - case_id: first-hatch-absolute-python-facts-version + scenario_id: independent-customer-interpreter-selection + method: test + intent: Require absolute Hatch declaration facts to match selected target. + observable: A mismatched base prefix or full version returns structured failure naming the declaration. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_absolute_hatch_python_must_match_selected_interpreter[version] + - case_id: first-hatch-numeric-version-components + scenario_id: independent-customer-interpreter-selection + method: test + intent: Compare numeric declarations by whole components during pure inspection. + observable: 3.11.1 and 3.11.15 conflict without native queries. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_numeric_hatch_and_version_file_compare_whole_components + - case_id: first-hatch-native-absolute-python-cold-without-version-file + scenario_id: independent-customer-interpreter-selection + method: test + intent: Use a native absolute Hatch interpreter declaration for existing and cold targets, with and without + version file. + observable: Native command base prefix and full version match the declared interpreter; declarations remain + unchanged. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_hatch_absolute_python_declaration[cold-without-version-file] + - case_id: first-hatch-native-absolute-python-cold-with-version-file + scenario_id: independent-customer-interpreter-selection + method: test + intent: Use a native absolute Hatch interpreter declaration for existing and cold targets, with and without + version file. + observable: Native command base prefix and full version match the declared interpreter; declarations remain + unchanged. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_hatch_absolute_python_declaration[cold-with-version-file] + - case_id: first-hatch-native-absolute-python-existing-without-version-file + scenario_id: independent-customer-interpreter-selection + method: test + intent: Use a native absolute Hatch interpreter declaration for existing and cold targets, with and without + version file. + observable: Native command base prefix and full version match the declared interpreter; declarations remain + unchanged. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_hatch_absolute_python_declaration[existing-without-version-file] + - case_id: first-hatch-native-absolute-python-existing-with-version-file + scenario_id: independent-customer-interpreter-selection + method: test + intent: Use a native absolute Hatch interpreter declaration for existing and cold targets, with and without + version file. + observable: Native command base prefix and full version match the declared interpreter; declarations remain + unchanged. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_hatch_absolute_python_declaration[existing-with-version-file] + - case_id: first-hatch-cold-created-absolute-base-identity + scenario_id: independent-customer-interpreter-selection + method: test + intent: Validate the native-created cold target against the absolute declared base interpreter, even at equal + full version. + observable: A changed created-target base prefix fails structurally before completion metadata is published. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_cold_absolute_hatch_python_validates_created_base + - case_id: first-hatch-explicit-numeric-python-matching + scenario_id: independent-customer-interpreter-selection + method: test + intent: Validate every explicitly requested numeric Python component. + observable: Mismatched patch refuses before native queries; a matching full version selects the existing target + without synchronization. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_explicit_numeric_python_checks_all_requested_components[matching] + - case_id: first-hatch-explicit-numeric-python-wrong-patch + scenario_id: independent-customer-interpreter-selection + method: test + intent: Validate every explicitly requested numeric Python component. + observable: Mismatched patch refuses before native queries; a matching full version selects the existing target + without synchronization. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_explicit_numeric_python_checks_all_requested_components[wrong-patch] openspec:code-review-17-project-runtime-bootstrap:project-runtime-bootstrap:faithful-external-sidecar-preparation: rationale: Review the owner-approved replacement contract at planned maturity without claiming implementation evidence. @@ -53,6 +196,49 @@ requirements: observable: the resulting dependency and origin identities are recorded externally and no lock or constraints are written into the customer project. Planning inspection only; runtime tests, RED/GREEN, installed acceptance and publication remain pending. + - case_id: first-hatch-existing-and-cold-native-hatch-workflow + scenario_id: faithful-external-sidecar-preparation + method: test + intent: Bounded first Hatch milestone. existing and cold native hatch workflow. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_existing_and_cold_native_hatch_workflow + - case_id: first-hatch-native-customer-storage + scenario_id: faithful-external-sidecar-preparation + method: test + intent: Discover a real existing native environment in customer-selected data and cache storage despite controller + contamination. + observable: The exact existing prefix executes with customer storage settings and without controller Hatch + selectors. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_hatch_customer_storage_survives_controller_context + - case_id: first-hatch-existing-target-workspace-inside + scenario_id: faithful-external-sidecar-preparation + method: test + intent: Keep explicit-base preparation storage outside the existing customer environment. + observable: A nested workspace is refused without allocation, receipt or environment mutation. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_explicit_base_existing_target_excludes_workspace[inside] + - case_id: first-hatch-existing-target-workspace-symlink + scenario_id: faithful-external-sidecar-preparation + method: test + intent: Resolve the existing target workspace before applying the same external-storage contract. + observable: A symlink into the selected environment is refused without allocation or receipt. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_explicit_base_existing_target_excludes_workspace[symlink] + - case_id: first-hatch-existing-target-workspace-external + scenario_id: faithful-external-sidecar-preparation + method: test + intent: Preserve valid external preparation with an explicit compatible base Python. + observable: A fresh external workspace publishes a receipt while the existing target remains unchanged. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_explicit_base_existing_target_excludes_workspace[external] openspec:code-review-17-project-runtime-bootstrap:project-runtime-bootstrap:single-project-execution-owner: rationale: Review the owner-approved replacement contract at planned maturity without claiming implementation evidence. @@ -77,6 +263,42 @@ requirements: observable: the chosen working directory, manager environment, script expansion and import roots match the project context rather than a controller heuristic. Planning inspection only; runtime tests, RED/GREEN, installed acceptance and publication remain pending. + - case_id: first-hatch-controller-python-path-removed-and-explicit-override-honored + scenario_id: single-project-execution-owner + method: test + intent: Bounded first Hatch milestone. controller python path removed and explicit override honored. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_controller_python_path_removed_and_explicit_override_honored + - case_id: first-hatch-nonzero-timeout-and-bounded-output-are-distinct + scenario_id: single-project-execution-owner + method: test + intent: Bounded first Hatch milestone. nonzero timeout and bounded output are distinct. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_nonzero_timeout_and_bounded_output_are_distinct + - case_id: first-hatch-unrelated-scripts-and-locks-do-not-block-existing-execution + scenario_id: single-project-execution-owner + method: test + intent: Bounded first Hatch milestone. unrelated scripts and locks do not block existing execution. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_unrelated_scripts_and_locks_do_not_block_existing_execution + - case_id: first-hatch-controller-python-startup-isolation + scenario_id: single-project-execution-owner + method: test + intent: Remove inherited controller Python startup settings while allowing explicit nonidentity overrides. + observable: Ambient optimization and startup values are absent in project execution, while explicit optimization + changes the child flags. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_controller_python_startup_removed_and_explicit_override_honored openspec:code-review-17-project-runtime-bootstrap:project-runtime-bootstrap:preparation-failure-and-reuse-are-explicit: rationale: Review the owner-approved replacement contract at planned maturity without claiming implementation evidence. @@ -101,6 +323,136 @@ requirements: observable: the partial or stale context is not treated as ready and a fresh owned preparation can proceed without modifying customer inputs. Planning inspection only; runtime tests, RED/GREEN, installed acceptance and publication remain pending. + - case_id: first-hatch-source-edits-allowed-but-dependency-drift-requires-reinspection + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Bounded first Hatch milestone. source edits allowed but dependency drift requires reinspection. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_source_edits_allowed_but_dependency_drift_requires_reinspection + - case_id: first-hatch-external-fresh-workspace-required + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Bounded first Hatch milestone. external fresh workspace required. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_external_fresh_workspace_required + - case_id: first-hatch-unreadable-execution-identity-before-command + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Fail structurally for fingerprint read failures before and after command execution. + observable: Before-command failure prevents execution; after-command failure retains output and reports execution_failed. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_unreadable_execution_identity_is_structured[before-command] + - case_id: first-hatch-unreadable-execution-identity-after-command + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Fail structurally for fingerprint read failures before and after command execution. + observable: Before-command failure prevents execution; after-command failure retains output and reports execution_failed. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_unreadable_execution_identity_is_structured[after-command] + - case_id: first-hatch-cleanup-preserves-primary-failure + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Retain the original preparation error when incomplete workspace cleanup also fails. + observable: Structured failure identifies cleanup residue and requires a fresh workspace. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_cleanup_failure_preserves_preparation_failure + - case_id: first-hatch-pytest-config-drift-pytest-toml + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Include pytest.toml in prepared project configuration identity. + observable: Changed pytest configuration refuses execution and changes both input and source provenance. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_pytest_config_drift_requires_reinspection[pytest.toml] + - case_id: first-hatch-pytest-config-drift-dot-pytest-toml + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Include .pytest.toml in prepared project configuration identity. + observable: Changed pytest configuration refuses execution and changes both input and source provenance. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_pytest_config_drift_requires_reinspection[.pytest.toml] + - case_id: first-hatch-pytest-config-drift-dot-pytest-ini + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Include .pytest.ini in prepared project configuration identity. + observable: Changed pytest configuration refuses execution and changes both input and source provenance. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_pytest_config_drift_requires_reinspection[.pytest.ini] + - case_id: first-hatch-pytest-config-drift-tox-ini + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Include tox.ini in prepared project configuration identity. + observable: Changed pytest configuration refuses execution and changes both input and source provenance. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_pytest_config_drift_requires_reinspection[tox.ini] + - case_id: first-hatch-coverage-config-drift + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Include coverage selection in prepared project configuration identity. + observable: Changed .coveragerc refuses execution and changes input and source provenance. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_coverage_config_drift_requires_reinspection + - case_id: first-hatch-directory-traversal-inspection-enumeration + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Reject incomplete project identities after directory enumeration fails during inspection. + observable: Traversal failure is structured, blocks unstarted commands and preserves output after execution. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_directory_traversal_failure_rejects_incomplete_identity[inspection-enumeration] + - case_id: first-hatch-directory-traversal-inspection-classification + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Reject incomplete project identities after directory classification fails during inspection. + observable: Traversal failure is structured, blocks unstarted commands and preserves output after execution. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_directory_traversal_failure_rejects_incomplete_identity[inspection-classification] + - case_id: first-hatch-directory-traversal-before-command-enumeration + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Reject incomplete project identities after directory enumeration fails during before-command. + observable: Traversal failure is structured, blocks unstarted commands and preserves output after execution. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_directory_traversal_failure_rejects_incomplete_identity[before-command-enumeration] + - case_id: first-hatch-directory-traversal-before-command-classification + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Reject incomplete project identities after directory classification fails during before-command. + observable: Traversal failure is structured, blocks unstarted commands and preserves output after execution. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_directory_traversal_failure_rejects_incomplete_identity[before-command-classification] + - case_id: first-hatch-directory-traversal-after-command-enumeration + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Reject incomplete project identities after directory enumeration fails during after-command. + observable: Traversal failure is structured, blocks unstarted commands and preserves output after execution. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_directory_traversal_failure_rejects_incomplete_identity[after-command-enumeration] + - case_id: first-hatch-directory-traversal-after-command-classification + scenario_id: preparation-failure-and-reuse-are-explicit + method: test + intent: Reject incomplete project identities after directory classification fails during after-command. + observable: Traversal failure is structured, blocks unstarted commands and preserves output after execution. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_directory_traversal_failure_rejects_incomplete_identity[after-command-classification] openspec:code-review-17-project-runtime-bootstrap:project-runtime-bootstrap:portable-inspection-and-bootstrap-commands: rationale: Review the owner-approved replacement contract at planned maturity without claiming implementation evidence. @@ -125,3 +477,171 @@ requirements: observable: it either reads an explicitly supported versioned local descriptor or returns a migration diagnostic, and never silently treats the descriptor path as an interpreter executable. Planning inspection only; runtime tests, RED/GREEN, installed acceptance and publication remain pending. + - case_id: first-hatch-inspection-is-pure-and-ambiguity-is-actionable + scenario_id: portable-inspection-and-bootstrap-commands + method: test + intent: Bounded first Hatch milestone. inspection is pure and ambiguity is actionable. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_inspection_is_pure_and_ambiguity_is_actionable + - case_id: first-hatch-selected-unsupported-context-rejected-before-native-query + scenario_id: portable-inspection-and-bootstrap-commands + method: test + intent: Bounded first Hatch milestone. selected unsupported context rejected before native query. + observable: Actual selected environment execution satisfies the test predicate; full manager and reviewer + rollout remains pending. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_selected_unsupported_context_rejected_before_native_query + - case_id: first-hatch-unreadable-inspection-identity + scenario_id: portable-inspection-and-bootstrap-commands + method: test + intent: Return an inspection diagnostic when fingerprint input cannot be read. + observable: No input identity is invented and preparation does not query Hatch. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_unreadable_inspection_identity_is_diagnostic + - case_id: first-hatch-custom-global-config-explicitly-unsupported + scenario_id: portable-inspection-and-bootstrap-commands + method: test + intent: Refuse unsupported custom global Hatch project context before native queries. + observable: HATCH_CONFIG appears in inspection diagnostics and no Hatch invocation occurs. + selector: + runner: pytest + node_id: tests/unit/specfact_codebase/test_project_runtime.py::test_custom_hatch_config_is_diagnostic_before_native_query + openspec:code-review-17-project-runtime-bootstrap:project-runtime-bootstrap:first-hatch-delivery-review-handoff: + rationale: Delivery verification for the explicitly approved Darwin-to-blocking-Linux review handoff. + stakeholder_refs: + - nold-ai/specfact-cli-modules#513 + touchpoints: + - id: project-runtime-bootstrap-spec + kind: config_file + locator: openspec/changes/code-review-17-project-runtime-bootstrap/specs/project-runtime-bootstrap/spec.md + verification_cases: + - case_id: first-hatch-portable-fixture-review-handoff-guards-staged-fixture + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Exercise staged fixture admission, unstaged fixture rejection and mandatory indexed independent review. + observable: Only the staged fixture with required indexed jobs reports DEFERRED and executes all remaining + Block 2 checks. + selector: + runner: pytest + node_id: tests/host/proof_capsule_deferred_review_ci.py::test_portable_fixture_deferral_retains_block2_guards[staged-fixture] + - case_id: first-hatch-portable-fixture-review-handoff-guards-unstaged-fixture + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Exercise staged fixture admission, unstaged fixture rejection and mandatory indexed independent review. + observable: Only the staged fixture with required indexed jobs reports DEFERRED and executes all remaining + Block 2 checks. + selector: + runner: pytest + node_id: tests/host/proof_capsule_deferred_review_ci.py::test_portable_fixture_deferral_retains_block2_guards[unstaged-fixture] + - case_id: first-hatch-portable-fixture-review-handoff-guards-missing-indexed-independent-review + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Exercise staged fixture admission, unstaged fixture rejection and mandatory indexed independent review. + observable: Only the staged fixture with required indexed jobs reports DEFERRED and executes all remaining + Block 2 checks. + selector: + runner: pytest + node_id: tests/host/proof_capsule_deferred_review_ci.py::test_portable_fixture_deferral_retains_block2_guards[missing-indexed-independent-review] + - case_id: first-hatch-optimized-proof-rejected-flag + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Refuse optimized local native proof before workspace allocation. + observable: Actual -O and PYTHONOPTIMIZE invocations report the named diagnostic and create no workspace or + receipt. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_proof_rejects_optimized_python_before_allocation[flag] + - case_id: first-hatch-optimized-proof-rejected-environment + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Refuse optimized local native proof before workspace allocation. + observable: Actual -O and PYTHONOPTIMIZE invocations report the named diagnostic and create no workspace or + receipt. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_proof_rejects_optimized_python_before_allocation[environment] + - case_id: first-hatch-wrong-controller-proof-rejected + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Refuse wrong proof controller before workspace allocation. + observable: An explicit controller diagnostic occurs before workspace or receipt exists. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_native_proof_rejects_wrong_controller_before_allocation + - case_id: first-hatch-configured-native-cell-missing0-workflow + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing0-workflow] + - case_id: first-hatch-configured-native-cell-missing0-storage + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing0-storage] + - case_id: first-hatch-configured-native-cell-missing0-absolute + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing0-absolute] + - case_id: first-hatch-configured-native-cell-missing1-workflow + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing1-workflow] + - case_id: first-hatch-configured-native-cell-missing1-storage + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing1-storage] + - case_id: first-hatch-configured-native-cell-missing1-absolute + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing1-absolute] + - case_id: first-hatch-configured-native-cell-missing2-workflow + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing2-workflow] + - case_id: first-hatch-configured-native-cell-missing2-storage + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing2-storage] + - case_id: first-hatch-configured-native-cell-missing2-absolute + scenario_id: first-hatch-delivery-review-handoff + method: test + intent: Reject an explicitly configured native cell with missing prerequisites before allocation. + observable: The selected workflow fails with each missing prerequisite name and creates no workspace. + selector: + runner: pytest + node_id: tests/integration/test_hatch_project_runtime.py::test_configured_native_cell_fails_missing_prerequisites[missing2-absolute] diff --git a/openspec/changes/code-review-17-project-runtime-bootstrap/specs/project-runtime-bootstrap/spec.md b/openspec/changes/code-review-17-project-runtime-bootstrap/specs/project-runtime-bootstrap/spec.md index b971df94..65e47a97 100644 --- a/openspec/changes/code-review-17-project-runtime-bootstrap/specs/project-runtime-bootstrap/spec.md +++ b/openspec/changes/code-review-17-project-runtime-bootstrap/specs/project-runtime-bootstrap/spec.md @@ -13,6 +13,26 @@ The runtime service SHALL select and probe the customer interpreter independentl - **WHEN** inspection resolves the project - **THEN** it names the candidates or missing version and performs no installation or controller-Python fallback +#### Scenario: Explicit base interpreter identity +- **GIVEN** the caller explicitly selects an absolute base Python executable +- **WHEN** an existing or newly created Hatch target reports the same Python version from a different base installation +- **THEN** preparation refuses with a diagnostic naming the selected and observed base installations +- **AND** it does not publish completion metadata or return an execution capability for the mismatched target + +#### Scenario: Explicit numeric interpreter selection +- **GIVEN** a caller explicitly selects a numeric Python version +- **WHEN** the executable found for its major/minor name reports a different requested component +- **THEN** preparation refuses with the requested and observed versions before native manager queries +- **AND** a matching full numeric version still selects the existing environment without synchronization + +#### Scenario: Absolute native Hatch Python declaration +- **GIVEN** a detached Hatch environment declares an absolute Python executable, optionally alongside a compatible .python-version +- **WHEN** pure inspection records the declarations and preparation selects an existing or cold environment +- **THEN** preparation probes the declared executable and validates the selected target's base interpreter and full version without rejecting the absolute path as a numeric version +- **AND** cold preparation uses the declared absolute executable before numeric discovery when no caller override is supplied +- **AND** conflicting numeric declaration components or incompatible actual interpreter facts fail with a diagnostic +- **AND** numeric declarations for an existing target do not require a separate host Python lookup + ### Requirement: Faithful external sidecar preparation Preparation SHALL use native pip or pip-tools, Hatch, uv, or Poetry semantics in SpecFact-owned external storage. It SHALL preserve locks, constraints, markers, selected groups and extras, editable behavior, local or VCS origins, build hooks, generated sources, and entrypoints. Existing customer environments SHALL remain execution targets without SpecFact package installation. @@ -21,6 +41,25 @@ Preparation SHALL use native pip or pip-tools, Hatch, uv, or Poetry semantics in - **WHEN** review uses an existing environment and then prepares the same project from a cold state - **THEN** both execute the selected project command, native pytest plugin, entrypoint and dependency imports with unchanged project declarations and customer environment inventory +#### Scenario: Existing Hatch execution does not synchronize dependencies +- **GIVEN** a selected existing Hatch environment has a verified compatible project interpreter and installed project dependencies +- **WHEN** preparation selects it and a project command executes +- **THEN** execution uses that environment without invoking dependency synchronization or installing SpecFact or analyzer packages +- **AND** the environment package inventory and project declarations remain unchanged + +#### Scenario: Existing Hatch targets exclude preparation storage +- **GIVEN** an existing selected Hatch environment and a compatible explicit base Python +- **WHEN** the invocation workspace resolves inside that environment, directly or through a symlink +- **THEN** preparation refuses before allocating the workspace or writing a receipt in the customer environment +- **AND** an external fresh workspace still prepares successfully without modifying the existing environment + +#### Scenario: Cold Hatch state stays outside the project +- **GIVEN** the selected detached Hatch environment does not exist and the required project interpreter and Hatch executable are available +- **WHEN** preparation creates the environment in SpecFact-owned external storage +- **THEN** native Hatch consumes the selected environment's declared dependencies using that interpreter +- **AND** the service does not inject SpecFact dependencies, rewrite project declarations or locks, or publish an interrupted preparation as ready +- **AND** source-only checks can retain their own results when required dynamic preparation fails + #### Scenario: Locked manager preparation - **GIVEN** a project supplies a manager lock or pip requirements and constraints - **WHEN** its selected adapter prepares an external environment @@ -31,9 +70,20 @@ Preparation SHALL use native pip or pip-tools, Hatch, uv, or Poetry semantics in - **WHEN** preparation resolves that declared project using its selected manager - **THEN** the resulting dependency and origin identities are recorded externally and no lock or constraints are written into the customer project +#### Scenario: Native Hatch storage selection survives controller isolation +- **GIVEN** a trusted project has an existing detached Hatch environment in customer-selected HATCH_DATA_DIR and HATCH_CACHE_DIR storage +- **WHEN** preparation discovers the environment while the controller carries unrelated active-environment and interpreter settings +- **THEN** native discovery uses the selected customer storage and project root without inheriting the controller environment selection or creating a replacement environment + ### Requirement: Single project execution owner specfact_codebase.project_runtime SHALL own inspection, preparation, and project command execution. Code Review and existing framework sidecar validation SHALL consume this service without a reverse dependency on Code Review. Commands SHALL bind argv, working directory, environment overrides, and a timeout. +#### Scenario: Controller Python startup isolation +- **GIVEN** the controller carries Python startup settings including optimization or user-base selection +- **WHEN** a project command executes without an explicit override +- **THEN** inherited Python startup variables do not change the selected interpreter's execution settings +- **AND** callers can explicitly supply nonidentity Python startup overrides through command environment overrides + #### Scenario: Existing sidecar consumes project context - **GIVEN** framework sidecar validation needs a project interpreter and dependencies - **WHEN** it prepares and executes a project command @@ -57,6 +107,36 @@ Preparation SHALL distinguish ready context from structured failure. Reuse SHALL - **WHEN** another invocation attempts reuse - **THEN** the partial or stale context is not treated as ready and a fresh owned preparation can proceed without modifying customer inputs +#### Scenario: Pytest configuration changes invalidate prepared identity +- **GIVEN** a prepared project using `pytest.toml`, `.pytest.toml`, `.pytest.ini`, or `tox.ini` +- **WHEN** the selected test configuration changes +- **THEN** the next command refuses stale preparation before execution and requires reinspection +- **AND** both the inspected input identity and reported source identity reflect the changed configuration + +#### Scenario: Coverage configuration changes invalidate prepared identity +- **GIVEN** a prepared project uses `.coveragerc` +- **WHEN** its coverage selection changes after preparation +- **THEN** execution requires reinspection before starting the command and both configuration and source identities reflect the change + +#### Scenario: Unreadable directories cannot produce verified identities +- **GIVEN** a project directory cannot be enumerated or classified during configuration or source fingerprinting +- **WHEN** inspection or execution fingerprints the project before or after a command +- **THEN** it reports the directory traversal failure instead of accepting an incomplete identity +- **AND** a failure before execution does not run the command, while a failure after execution preserves its captured output + +#### Scenario: Unreadable execution identity fails structurally +- **GIVEN** a prepared project has a source or configuration file that cannot be read +- **WHEN** execution fingerprints the project before or after running a command +- **THEN** it returns execution_failed with the read diagnostic rather than raising an exception or claiming a verified unreadable identity +- **AND** a failure before execution does not run the command, while a failure after execution preserves captured stdout and stderr alongside a visible diagnostic +- **AND** combined stderr remains bounded and reports truncation when needed + +#### Scenario: Cleanup failure preserves the preparation failure +- **GIVEN** preparation fails after creating its invocation-owned workspace +- **WHEN** removing the incomplete workspace also fails +- **THEN** preparation returns its original structured failure and identifies the retained workspace and cleanup error in the remedy +- **AND** it requires a fresh workspace for another preparation attempt + ### Requirement: Portable inspection and bootstrap commands code review runtime inspect SHALL remain read-only and runtime prepare SHALL initialize external sidecar state. Ordinary review SHALL prepare missing context automatically. Python and platform capability SHALL be evaluated independently of capsule ABI or architecture catalogs. @@ -69,3 +149,44 @@ code review runtime inspect SHALL remain read-only and runtime prepare SHALL ini - **GIVEN** a caller supplies the legacy --project-runtime descriptor option - **WHEN** the replacement command parses it - **THEN** it either reads an explicitly supported versioned local descriptor or returns a migration diagnostic, and never silently treats the descriptor path as an interpreter executable + +#### Scenario: Unreadable inspection identity remains diagnostic +- **GIVEN** a project declaration selected for fingerprinting cannot be read +- **WHEN** inspection runs +- **THEN** the plan contains a diagnostic and no invented input identity, and preparation rejects the plan before querying Hatch + +#### Scenario: Custom global Hatch configuration is explicitly unsupported +- **GIVEN** HATCH_CONFIG selects a custom global configuration that can alter native project context +- **WHEN** this first detached Hatch milestone inspects or prepares the project, including a changed selection after inspection +- **THEN** it reports that custom global configuration is unsupported before native queries rather than silently ignoring the selection + +### Requirement: First Hatch delivery review handoff +For delivery verification of the first Hatch milestone, the approved local ARM64 Darwin feature-worktree exception SHALL recognize staged portable-runtime fixture changes that already schedule the blocking capsule workflow. It SHALL defer only capsule review and retain the existing platform, CI, branch, worktree, indexed workflow, independent job and candidate guards. All other Block 2 checks SHALL still execute. This requirement covers delivery verification only and does not claim public runtime CLI adoption or completion of change 17. + +#### Scenario: Staged portable runtime fixture schedules mandatory review +- **GIVEN** an approved local ARM64 Darwin feature worktree with both required indexed review jobs and a staged portable-runtime fixture change +- **WHEN** Block 2 runs with the GitHub Linux review exception +- **THEN** capsule review reports DEFERRED rather than PASS and all other Block 2 checks and contracts execute +- **AND** exact-head hosted review remains mandatory before merge + +#### Scenario: Unstaged fixture cannot authorize deferral +- **GIVEN** only the working tree contains the portable-runtime fixture change and no indexed candidate path schedules capsule review +- **WHEN** Block 2 requests the exception +- **THEN** it rejects deferral with the candidate scheduling diagnostic + +#### Scenario: Indexed independent review remains mandatory +- **GIVEN** a staged portable-runtime fixture change but the indexed workflow lacks the independent signed-review job +- **WHEN** Block 2 requests the exception +- **THEN** it rejects deferral with the missing independent review diagnostic + +#### Scenario: Local native proof preconditions precede allocation +- **GIVEN** the local native Hatch proof relies on executable assertions and a Python 3.12 controller +- **WHEN** it is invoked with optimized Python or another controller version +- **THEN** it refuses with an explicit diagnostic before creating the proof workspace or receipt +- **AND** the local proof remains development evidence rather than release or protected CI authority + +#### Scenario: Configured native acceptance cell cannot skip missing prerequisites +- **GIVEN** SPECFACT_HATCH_PROOF_WORKSPACE explicitly enables the native acceptance cell +- **WHEN** any mapped native Hatch workflow lacks SPECFACT_HATCH_EXECUTABLE or SPECFACT_HATCH_PROJECT_PYTHON +- **THEN** that workflow fails with the missing prerequisite names before allocating a customer or invocation workspace +- **AND** an unconfigured full-suite run may still skip native cases whose prerequisites are absent diff --git a/openspec/changes/code-review-17-project-runtime-bootstrap/tasks.md b/openspec/changes/code-review-17-project-runtime-bootstrap/tasks.md index e63edc90..889d0304 100644 --- a/openspec/changes/code-review-17-project-runtime-bootstrap/tasks.md +++ b/openspec/changes/code-review-17-project-runtime-bootstrap/tasks.md @@ -1,6 +1,6 @@ # Tasks for code-review-17-project-runtime-bootstrap -All boxes are future implementation. This artifact delivery completes no runtime task and creates no RED/GREEN or installed-support evidence. +The full-change checklist remains pending beyond the explicitly bounded checkpoint below. The first Hatch milestone does not complete this change, migrate ordinary consumers or establish installed/release acceptance. ## Readiness and specification @@ -31,3 +31,14 @@ All boxes are future implementation. This artifact delivery completes no runtime - [ ] Apply required semver, CHANGELOG.md, compatibility, module payload/signature and canonical publication steps only when runtime assets change. - [ ] Demonstrate the exact installed user workflow and rollback for the owning scope; keep unavailable cells and required failures nonaccepting. - [ ] Submit implementation for normal review to dev, reconcile issue/project state and archive only after actual completion using openspec archive. + +## Authorized first Hatch checkpoint, 2026-10-23 + +- [x] Confirm owner takeover, public hierarchy/dependencies and the bounded existing/cold trusted-local use case. +- [x] Preserve the real legacy wrong-interpreter RED before production and retain later genuine constraint/default-discovery RED evidence. +- [x] Implement the Codebase-owned detached Hatch execution capability without changing public review or legacy sidecar consumers. +- [x] Prove existing and cold target Python 3.11.15 from controller 3.12.13, actual imports/entrypoint/native async tests/customer lint/seeded failure, immutable declarations and inventory. +- [x] Reconcile exact mapped executable selectors with genuine independent local technical acceptance and actual JUnit through the canonical Requirements lifecycle using the honest legacy-TDD transition record. +- [x] Complete bounded independent implementation review and resolve its three reproduced corrections. Required SpecFact and CodeRabbit review prerequisites remain separate open gates. +- [ ] Finish all mandatory quality gates and retain any incomplete review prerequisites explicitly. +- [ ] Complete remaining native script/context fidelity, other managers, consumer migrations and full change-17 acceptance before archive. diff --git a/openspec/changes/code-review-hatch-pr-review-fixes/.openspec.yaml b/openspec/changes/code-review-hatch-pr-review-fixes/.openspec.yaml new file mode 100644 index 00000000..7ccf7c36 --- /dev/null +++ b/openspec/changes/code-review-hatch-pr-review-fixes/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-25 diff --git a/openspec/changes/code-review-hatch-pr-review-fixes/TDD_EVIDENCE.md b/openspec/changes/code-review-hatch-pr-review-fixes/TDD_EVIDENCE.md new file mode 100644 index 00000000..08360034 --- /dev/null +++ b/openspec/changes/code-review-hatch-pr-review-fixes/TDD_EVIDENCE.md @@ -0,0 +1,31 @@ +# Native PR review correction evidence + +Recorded 2026-10-25, Europe/Berlin, against signed base `8171f496711e436e6e788b35b1d3434368c0f3e5`. + +## Confirmed scope + +The owner requested PR #524 merge readiness with native-only implementation, signing and independent gates preserved. Open threads `PRRT_kwDORVEFbs6v8Unx` and `PRRT_kwDORVEFbs6wBmka` report directory alias retargeting and a baseline commit lost after squash. Exact-head Linux also reports Radon complexity in three native tests. Capsule runtime/tooling/admission repairs and reviewer cutover remain excluded. + +## Actual failing-first sequence + +Spec and tests preceded source edits. The first draft run included a missing required timeout in a newly authored control; that test defect is not production RED. Corrected `/private/tmp/513-babysit-red-corrected.xml` contains seven intended failures and one already passing physical-target content control. Both identities remained equal after alias retargeting. All six final-tree-only checkout cases failed on `freeze_ref comparison unavailable` instead of performing the required comparison. Independent pre-code eight-selector acceptance and actual test-authored evaluation are `/private/tmp/513-babysit-mapping-{acceptance,evaluation}.json`. + +Implementation retained the physical traversal and added directory-link bindings. The guard now carries the exact 167 classified mode/blob records from its existing freeze in the committed policy. Its default path does not resolve that historical commit. A disposable second Git repository contains only the final tree and proves the original commit is absent. Historical CLI comparisons still validate revisions safely. An inert, provenance-labeled exact excerpt of the real earlier capsule repair replaces the suite's dependency on disappearing feature history; the original full historical comparison remains prior recorded evidence, not a newly claimed portable checkout. + +Independent review found missed staged chmod when `core.filemode=false`; CodeRabbit found CRLF normalization. Spec and two mapped tests preceded both corrections. `/private/tmp/513-babysit-portability-red.xml` records two intended failures. Fresh ten-selector technical acceptance is `/private/tmp/513-babysit-portability-mapping-acceptance.json`. This acceptance precedes the two corrections, not the initial eight-case implementation. Regular file identities now use Git clean filters and actual index modes when filesystem mode tracking is disabled; symlinks hash raw link text. These retain the previous trusted-local Git semantics. + +All 144 nearby runtime, scope, native workflow and unchanged prerequisite cases pass in `/private/tmp/513-babysit-portability-green.xml`. Three native test functions were decomposed with their collected selectors and assertions retained. Direct Radon measurement finds no function above complexity 10 in the affected checker and two native test files. Actual local execution of the unchanged native workflow passes all 59 mapped cases with zero skips; receipt/JUnit provenance is `.audit/513-babysit-native-real-run.json`. + +The final ten exact mapped cases pass in `/private/tmp/513-babysit-exact-green.xml`. Requirements reconciliation uses an explicit incremental legacy-TDD ledger, not a fabricated all-case initial RED. Canonical local records are `.audit/513-babysit-{plan,tdd-ledger,tdd-record,reconciliation,tested-source}.json`. + +Independent final engineering review accepted the staged corrections after checking mode/CRLF parity, directory replacement rejection, all 167 exact records and the preserved 24 workflow assertions. `/private/tmp/513-babysit-implementation-review.md` records the actual probes and scope limits. CodeRabbit completed final review of all seventeen staged files. Its remaining major staged-blob suggestion is dismissed as a bypass of supported mandatory paths: a fresh disposable fixture using the actual installed `pre_commit.staged_files_only` rejects the staged repair, restores the working tree, and committed-tree checking rejects it too. `.audit/513-babysit-staged-boundary.txt` records results 0/1/1 for standalone working-tree, actual pre-commit and committed-tree modes. Standalone working-tree mode retains its documented limitation; no new index-only admission rule is added. + +## Signing and delivery limits + +The runtime edit changes the codebase payload. Its development checksum is refreshed through the documented dev policy. The existing unreleased version 0.42.0 remains above dev 0.41.16; this is another correction within the same PR. Local release-key variables are unavailable. Canonical cryptographic module signing remains with the trusted-review CI signer, and no strict-signature PASS is claimed for the changed payload. Human commits remain GPG-signed as djm81. No signing workflow, key, registry or publication policy is changed. + +Required final gates, final independent review and delivery are recorded only after completion. The first SMART cycle passed 5,892 tests before the two later portability cases/corrections; its orchestrator was stopped before full testing. It is not final-source evidence. The final source needs its own gate record. + +The final source gate cycle then passed format, types, lint, YAML, actionlint, import boundaries, development module payload integrity/version verification, contracts, SMART, full testing and the full staged pipeline. SMART and full each passed 5,894 tests with 77 existing opt-in skips, five baseline warnings and 95 passing subtests. The pipeline passed Requirements and 28 contract cases. `.audit/513-babysit-terminal-gate-status.json` and adjacent logs record actual exit codes. Development integrity verification uses the existing dev policy; canonical cryptographic module signing is still pending trusted-review CI. Only the approved local Darwin capsule component is DEFERRED, never PASS. Signed delivery and exact-head hosted status are recorded in the PR after push. + +At base head, candidate 3.12 fails with pytest FileNotFoundError observations, native test complexity and incomplete targeted coverage. Independent signed review times out with analysis_timeout and missing_report. Quality fails on the existing customer-capsule prerequisite. These remain actual blockers; no repair, retry, threshold increase or replacement verdict is made here. diff --git a/openspec/changes/code-review-hatch-pr-review-fixes/design.md b/openspec/changes/code-review-hatch-pr-review-fixes/design.md new file mode 100644 index 00000000..83b8faf0 --- /dev/null +++ b/openspec/changes/code-review-hatch-pr-review-fixes/design.md @@ -0,0 +1,11 @@ +# Bounded review corrections + +Project provenance keeps the current physical traversal and adds each nonignored directory link's relative path and link text to both identity digests. Existing physical target configuration files remain content-fingerprinted. Traversing arbitrary aliases would change the read closure, cycle handling and cost; rejecting aliases would add admission restrictions. Neither is part of this correction. + +The scope policy remains the one owner of classified surfaces and reviewed state. A committed manifest stores each baseline path's Git mode and blob identity; the existing policy SHA binding covers it. Default checking compares current classified entries with those records and accepts missing entries as retirement. It never needs the old freeze commit. Explicit historical comparisons retain safe revision resolution and Git diff semantics. Modes preserve chmod/type detection; symlinks use link text, not target content. This is an accidental-drift guard, not hostile-candidate authentication. + +Alternatives are retaining/fetching a new remote ref, which adds operational setup and retention dependencies, or using dev as the baseline, which loses already approved feature-branch state. A committed manifest preserves the existing baseline without either dependency. Independent design review is `/private/tmp/513-babysit-design-review.md`. + +The decisive portability test copies only a final tree to a new Git repository with a single root commit. The old freeze object is absent. Unchanged/native/deletion cases must pass; repaired/new/mode-changed classified files must fail. Historical recurrence evidence remains honest about which original bytes or excerpts it preserves. + +Test decomposition retains collected selectors and assertions wherever possible. Static complexity is measured by the existing Radon analyzer, with no new runtime abstraction or acceptance threshold. diff --git a/openspec/changes/code-review-hatch-pr-review-fixes/proposal.md b/openspec/changes/code-review-hatch-pr-review-fixes/proposal.md new file mode 100644 index 00000000..b5c1cb29 --- /dev/null +++ b/openspec/changes/code-review-hatch-pr-review-fixes/proposal.md @@ -0,0 +1,18 @@ +# Resolve native Hatch PR review findings + +## Why + +The owner requested merge readiness for PR #524 while retaining the native-only direction, signing and independent security. Two open review findings identify missing directory-symlink bindings in project provenance and a migration baseline that disappears after squash merge. The exact-head candidate also reports complexity regressions in native test functions. + +## What changes + +- Fingerprint nonignored directory-symlink bindings so in-project retargeting cannot preserve stale provenance. +- Keep the reviewed migration baseline as committed path, Git mode and blob identities, independent of feature-branch history retention. +- Preserve deletion allowance, addition/edit rejection, policy fingerprint binding and safe explicit historical comparisons. +- Decompose the affected native tests without changing assertions or acceptance. + +## Scope and limits + +This is a correction to the first existing/cold detached Hatch milestone and its native migration guard. It does not introduce alias traversal, new admission restrictions, capsule repairs, ordinary reviewer cutover, another manager, publication or security-test ownership transfer. Existing project traversal exclusions remain effective; binding fingerprints do not claim complete content closure for external or ignored alias targets. Native local and Linux acceptance remain separate from independent protected review. + +The remaining capsule failures and independent timeout remain blocking. Merge readiness cannot be claimed until all applicable requirements pass. No merge or archive is authorized. diff --git a/openspec/changes/code-review-hatch-pr-review-fixes/requirements-evidence.yaml b/openspec/changes/code-review-hatch-pr-review-fixes/requirements-evidence.yaml new file mode 100644 index 00000000..3df85f90 --- /dev/null +++ b/openspec/changes/code-review-hatch-pr-review-fixes/requirements-evidence.yaml @@ -0,0 +1,145 @@ +{ + "schema_version": "2", + "requirements": { + "openspec:code-review-hatch-pr-review-fixes:native-hatch-review-corrections:directory-alias-retargeting-invalidates-in-project-provenance": { + "rationale": "Fix verified native PR findings without capsule repairs, admission changes or security gate waivers.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-review-corrections", + "kind": "config_file", + "locator": "openspec/changes/code-review-hatch-pr-review-fixes/specs/native-hatch-review-corrections/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "directory-alias-retargeting-invalidates-in-project-provenance-1", + "scenario_id": "directory-alias-retargeting-invalidates-in-project-provenance", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_codebase/test_project_runtime.py::test_directory_alias_retargeting_invalidates_prepared_identity" + } + }, + { + "case_id": "directory-alias-retargeting-invalidates-in-project-provenance-2", + "scenario_id": "directory-alias-retargeting-invalidates-in-project-provenance", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_codebase/test_project_runtime.py::test_directory_alias_target_configuration_remains_fingerprinted" + } + } + ] + }, + "openspec:code-review-hatch-pr-review-fixes:native-hatch-review-corrections:migration-scope-survives-normal-squash-integration": { + "rationale": "Fix verified native PR findings without capsule repairs, admission changes or security gate waivers.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-review-corrections", + "kind": "config_file", + "locator": "openspec/changes/code-review-hatch-pr-review-fixes/specs/native-hatch-review-corrections/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "migration-scope-survives-normal-squash-integration-1", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_survives_squash_only_checkout[unchanged]" + } + }, + { + "case_id": "migration-scope-survives-normal-squash-integration-2", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_survives_squash_only_checkout[native]" + } + }, + { + "case_id": "migration-scope-survives-normal-squash-integration-3", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_survives_squash_only_checkout[retirement]" + } + }, + { + "case_id": "migration-scope-survives-normal-squash-integration-4", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_survives_squash_only_checkout[repair]" + } + }, + { + "case_id": "migration-scope-survives-normal-squash-integration-5", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_survives_squash_only_checkout[addition]" + } + }, + { + "case_id": "migration-scope-survives-normal-squash-integration-6", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Exercise the real native service or mandatory checker with a disposable project or final-tree-only Git repository.", + "observable": "Alias binding drift is nonaccepting; the native guard survives squash without allowing classified edits.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_survives_squash_only_checkout[mode]" + } + }, + { + "case_id": "migration-scope-survives-normal-squash-integration-7", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Preserve native Git checkout and staged-mode comparison behavior after baseline retention changes.", + "observable": "Unchanged CRLF checkout passes; staged chmod remains nonaccepting even with core.filemode false.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_preserves_git_checkout_normalization" + } + }, + { + "case_id": "migration-scope-survives-normal-squash-integration-8", + "scenario_id": "migration-scope-survives-normal-squash-integration", + "method": "test", + "intent": "Preserve native Git checkout and staged-mode comparison behavior after baseline retention changes.", + "observable": "Unchanged CRLF checkout passes; staged chmod remains nonaccepting even with core.filemode false.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_rejects_staged_mode_when_filemode_disabled" + } + } + ] + } + } +} diff --git a/openspec/changes/code-review-hatch-pr-review-fixes/specs/native-hatch-review-corrections/spec.md b/openspec/changes/code-review-hatch-pr-review-fixes/specs/native-hatch-review-corrections/spec.md new file mode 100644 index 00000000..e5f05603 --- /dev/null +++ b/openspec/changes/code-review-hatch-pr-review-fixes/specs/native-hatch-review-corrections/spec.md @@ -0,0 +1,31 @@ +## ADDED Requirements + +### Requirement: Directory alias retargeting invalidates in-project provenance +Project input and source fingerprints SHALL record nonignored directory symlink bindings without following directory links. Retargeting an alias between in-project configuration directories SHALL invalidate preparation before command execution. Relevant physical target configuration content SHALL remain fingerprinted. Existing traversal exclusions and error behavior SHALL remain effective. + +#### Scenario: Retarget an in-project test configuration alias +- **GIVEN** two project directories containing different pytest configuration and a directory alias selecting one +- **WHEN** the alias is retargeted after preparation +- **THEN** both identities change and the next command returns a stale-configuration failure before executing + +#### Scenario: Change configuration at the selected physical target +- **WHEN** a relevant configuration file changes in the physically traversed target directory +- **THEN** the alias does not hide that input change + +### Requirement: Migration scope survives normal squash integration +The default native scope check SHALL use a committed reviewed baseline that remains available in a final-tree-only Git checkout. It SHALL preserve classified additions, content edits, type and executable-mode changes as nonaccepting, allow unchanged/native work and retirement deletions, and retain policy fingerprint binding. Explicit historical comparison SHALL continue to validate revision arguments safely and fail when requested history is unavailable. The old freeze revision SHALL remain audit metadata rather than a default runtime dependency. + +#### Scenario: Verify a squash-only checkout +- **GIVEN** a new Git repository containing only the final committed tree and no feature-branch history +- **WHEN** mandatory native scope checking runs +- **THEN** unchanged/native/deletion states pass while classified edits, additions and mode changes fail + +#### Scenario: Retain policy and revision protections +- **WHEN** a policy-only mutation weakens the reviewed baseline or a historical argument is option-shaped +- **THEN** checking fails without interpreting revision text as a Git option + +#### Scenario: Preserve Git checkout and index semantics +- **WHEN** Git materializes unchanged classified files with CRLF line endings +- **THEN** their cleaned Git identities remain accepted +- **WHEN** a classified executable mode is changed in the index while filesystem mode tracking is disabled +- **THEN** the staged mode change remains rejected diff --git a/openspec/changes/code-review-hatch-pr-review-fixes/tasks.md b/openspec/changes/code-review-hatch-pr-review-fixes/tasks.md new file mode 100644 index 00000000..003a9f33 --- /dev/null +++ b/openspec/changes/code-review-hatch-pr-review-fixes/tasks.md @@ -0,0 +1,11 @@ +# Tasks + +- [x] Triage exact-head review threads and CI without capsule repairs or retries. +- [x] Compare bounded corrections and preserve trust/scope limits. +- [x] Add mapped regressions and capture genuine RED before source edits. +- [x] Implement directory binding and durable baseline corrections. +- [x] Decompose native test complexity without weakening assertions. +- [x] Verify focused regressions, local quality gates, development integrity/version and independent engineering review. +- [ ] Complete canonical module signing through trusted-review CI. +- [ ] Deliver signed fixes, resolve validated threads and inspect exact-head Linux. +- [ ] Obtain the separately required native cutover decision if legacy blockers remain. diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/.openspec.yaml b/openspec/changes/code-review-linux-analyzer-diagnostics/.openspec.yaml new file mode 100644 index 00000000..7ccf7c36 --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-25 diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/INVESTIGATION.md b/openspec/changes/code-review-linux-analyzer-diagnostics/INVESTIGATION.md new file mode 100644 index 00000000..e8652d6c --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/INVESTIGATION.md @@ -0,0 +1,55 @@ +# Linux analyzer investigation + +Observed 2026-10-25, Europe/Berlin. Subject head `59611f358572aaa3c383c2d2fffcf82fb57ff36c`; base `430e3ccbf9fbd6a6177b033662a76235a8bd6cc8`. This is source inspection plus bounded public CI evidence and labeled host diagnostics. It is not a successful Linux acceptance run. + +## Findings and confidence + +| Observation | Established cause or limit | Confidence | +| --- | --- | --- | +| Six native Hatch acceptance cases skip at call phase in candidate and independent jobs | The tests require explicit native proof workspace/executable/Python inputs. The sealed analyzer clears the environment and does not supply those inputs. Tests skip before invoking production proof behavior. | High for source flow and matching skip pattern. | +| Pytest tool error points at `scripts/prove_hatch_project_runtime.py:1` | Coverage evaluation uses the first selected source as an error anchor. That location does not prove the script ran or caused the failure. The actual protected exception remains unavailable. | High for anchor behavior; underlying Linux cause unknown. | +| Independent contracts analyzer has error/UNKNOWN | Process/import errors, execution errors, timeouts, and unrecognized output can all produce this result. Structured-report projection omits the available class and side observations. | High for incomplete evidence and projection gap; actual exception unknown. | +| Four Pylint warnings match base-only host emissions | The public projection drops lifecycle and blocking fields. A pure classifier diagnostic marks the four base-only rows fixed/non-blocking. Released index review also uses paired differential classification, so retained base findings are a supported explanation. Actual hosted row lifecycle remains unavailable. | High for lossy projection and index flow; hosted attribution unresolved. | + +The confirmed root causes are missing native acceptance prerequisites and loss of diagnostic provenance. I don't know yet which separate exceptions caused Linux pytest and contracts incompleteness. The required next evidence is the actual analyzer side and fixed failure class from the same protected execution. No analyzer repair is justified by the current public anchor or a Darwin import failure. + +## Execution boundaries + +Linux preparation uses `runtime_builder.py` and `runtime_build_driver.py`. It installs Hatch in preparation tooling, builds the customer environment, and copies customer dependencies/executables into the sealed artifact. It does not promise that the sealed analyzer can invoke native acceptance tooling. Historical traces through `native_project_runtime.py` describe Darwin and cannot establish the Linux launch path. + +The sandbox mounts prepared runtime content read-only, clears the environment, and sets a bounded analyzer PATH. Passing host `SPECFACT_HATCH_*` values or executable paths into it would change the boundary. Native acceptance also requires a compatible controller, project Python 3.11.15, and a fresh external workspace. A PREPARED descriptor alone proves none of those acceptance-cell resources. + +The independent job installs CLI 0.55.4 and released reviewer 0.51.2, captures the subject without executing its host code, and reviews index scope. Candidate implementation and independent reviewer implementation are distinct. Named pytest command/environment and sandbox functions in the cached released archive match the candidate AST, but this is not whole-runner equivalence or authenticated proof of hosted loaded bytes. + +## Diagnostic reproductions and their limits + +A controlled Darwin Python 3.12.13/CrossHair 0.0.107 probe exits 2 when the proof script cannot import `specfact_codebase`. Adding the Codebase source root changes that probe to exit 0. Linux uses pinned CrossHair 0.0.109 and injects descriptor-recorded source roots. The host result therefore proves its own ambient import omission, not the protected Linux cause. + +The actual host Pylint adapter emits four C0415 warnings on the base file and none on the head file. Feeding those observations and committed source bytes into the pure differential classifier returns PASS with four fixed findings. Candidate and released 0.51.2 retain fixed findings with explicit `blocking=false` in differential report mapping. Direct inspection of the downloaded 0.51.2 archive confirms that index review materializes HEAD and index snapshots and invokes paired differential classification, including its portable project-runtime path. The workflow's soft reset makes HEAD the merge-base while retaining the candidate index bytes. Historical fixed rows are therefore a supported explanation. The actual hosted rows still require lifecycle/side evidence. Do not infer a classifier bug or dismiss active findings from this diagnostic. + +The source error projection already defines `incomplete_sides` and `crosshair_failure_observations`, but calls them only for oversized independent reports. The observed independent report is structured. This deterministically explains why the useful class/side discriminators are absent from that public summary. Existing artifact upload paths exclude the private review report and log; the available uploaded artifact cannot supply their hidden exception details. + +## Exact-head public evidence + +These primary CI sources were read through the Code Review connector on 2026-10-25. Returned log tails were truncated, so absence from those tails is not proof that an analyzer passed. + +- [Candidate Python 3.12 job](https://github.com/nold-ai/specfact-cli-modules/actions/runs/40135941692/job/120997951923): failed; both descriptors prepared/present; pytest tool error and six native skips. Namespace audit captured without observed AppArmor/user-namespace denial markers. +- [Independent signed review](https://github.com/nold-ai/specfact-cli-modules/actions/runs/40135941692/job/120997951869): failed; structured report, overall FAIL, assurance UNKNOWN, contracts and targeted-pytest-coverage error/UNKNOWN. +- Other matrix jobs were still reported in progress on the latest read. This investigation makes no overall success claim and does not retry unchanged failed jobs. + +## Primary source map + +All links pin the subject revision and were inspected on 2026-10-25. + +- [Native prerequisite guards](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/tests/integration/test_hatch_project_runtime.py#L26): main case; storage lines 86-97 and absolute-Python cases lines 237-250. +- [Linux preparation](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py#L267) and [build driver](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py#L226). +- [Cleared sandbox environment](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/packages/specfact-code-review/src/specfact_code_review/run/sandbox.py#L502). +- [Pytest error anchor](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/packages/specfact-code-review/src/specfact_code_review/run/runner.py#L7190). +- [Contract command/result handling](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/packages/specfact-code-review/src/specfact_code_review/tools/contract_runner.py#L259) and [Linux target source roots](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py#L270). +- [Finding lifecycle mapping](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/packages/specfact-code-review/src/specfact_code_review/run/runner.py#L8079). +- Released 0.51.2 archive members inspected directly: `run/commands.py:1215-1216` dispatches immutable index review; `run/scope.py:2732-2742` materializes HEAD/index snapshots; `run/portable_snapshot.py:332-359` classifies the index pair. These released-source observations are distinct from authenticated hosted execution evidence. +- [Independent reviewer identity and invocation](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/.github/workflows/capsule-customer-execution.yml#L650), [lossy finding projection](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/.github/workflows/capsule-customer-execution.yml#L938), and [oversized-only class/side projection](https://github.com/nold-ai/specfact-cli-modules/blob/59611f358572aaa3c383c2d2fffcf82fb57ff36c/.github/workflows/capsule-customer-execution.yml#L1124). + +## Decision + +Recommend the [diagnostic-only proposal](proposal.md). Preserve every existing gate and trust boundary. If those observations reveal a runtime admission, dependency, timeout, or acceptance ownership issue, return a separate scope decision before repair. Strict proposal validation cannot establish a working customer reviewer or green Linux CI. diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/README.md b/openspec/changes/code-review-linux-analyzer-diagnostics/README.md new file mode 100644 index 00000000..78a8291b --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/README.md @@ -0,0 +1,3 @@ +# code-review-linux-analyzer-diagnostics + +Expose bounded Linux analyzer failure provenance without changing execution or gate authority diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/TDD_EVIDENCE.md b/openspec/changes/code-review-linux-analyzer-diagnostics/TDD_EVIDENCE.md new file mode 100644 index 00000000..f0a85d8d --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/TDD_EVIDENCE.md @@ -0,0 +1,72 @@ +# Evidence status + +Proposal prepared and diagnostic-only implementation approved on 2026-10-25, Europe/Berlin. Signed delivery and Linux repair are not established by local test evidence. + +The investigation uses PR #524 head `59611f358572aaa3c383c2d2fffcf82fb57ff36c` and base `430e3ccbf9fbd6a6177b033662a76235a8bd6cc8`. Existing milestone evidence remains with change 17 and is not reclassified as evidence for this proposal. + +Requirements mappings now contain 126 exact executable selectors across four requirements and ten specification scenarios. Independent mapping review accepted all selectors against actual collection and the repository lifecycle validator before production edits. A diagnostic replay is never a replacement for the original required review decision. + +## Proposal verification + +- `openspec validate code-review-linux-analyzer-diagnostics --strict --json --no-interactive` passed with no issues using OpenSpec 1.13.2. +- A local reproducible planning audit at `/private/tmp/513-check-diagnostic-proposal.py` checked all four requirements and ten scenarios against ten inspection mappings. It also verified unchanged tracked files and the exact subject HEAD. This is artifact inspection, not executed acceptance. +- `git diff --check` passed. The original governance archive worktree was left untouched. No runtime, workflow, test, dependency, module, registry, index, commit, or push change was made by this investigation. +- Independent read-only grounding review caught and corrected wording that could imply the hosted Pylint rows were proven fixed. Their actual lifecycle remains unresolved. + +## Failing-first evidence + +Actual workflow projection snippets ran with synthetic private reports and real temporary Git repositories. No Bash or capsule launch occurs in the new fixture tests. The final source-bound RED command was: + +```console +.venv/bin/python -m pytest tests/unit/test_capsule_analyzer_diagnostics.py -o addopts= -q -o junit_family=legacy --junitxml=/private/tmp/513-diagnostics-final-red.xml +``` + +Result: 50 failed, 76 passed, zero skipped, zero errors. Every failure is a missing intended diagnostic field/classification, including `None` versus recorded `base`/`head`, absent lifecycle/strict Boolean fields, and unclassified known pytest producer wrappers. The 76 passing cases are existing privacy, bounds, classification, and authority controls. Do not claim all 126 were RED. + +The unchanged workflow SHA-256 was `f048789a790ffa79d33075be48aa3451dac2478c5b816099ee6766fef2384137`. Final RED JUnit SHA-256 was `3366318437d907027f556cceb26370ab08e59e08286e3b640ecdc1134aecfa64`. Mapping digest was `sha256:fa140be021ab83cfb98f7daf1d862b2733a1ec00c680dd723adbdb9b446f1bf0`. The local source manifest, plan, and independent acceptance are under `.audit/513-diagnostics-{red-source,plan,mapping-acceptance}.json`. + +Earlier genuine 50-failure runs are retained under `/private/tmp/513-diagnostics-red.*` and `/private/tmp/513-diagnostics-valid-selectors-red.*`. Generated selector IDs were shortened to satisfy the existing validator, and the established `specfact.selector` test property was added before the final run. Neither correction changed test predicates or production behavior. These preliminary runs are not substituted for the final source-bound proof. + +Existing exact-output expectations are updated only for newly visible structured-report fields. Every existing scenario/assertion remains enabled. The required gates prevent committing intentionally failing tests, so immutable local source/JUnit records preserve RED-before-production order instead of a failing commit. + +## Passing-after evidence + +The same 126 exact selectors passed after the inline projection change, with zero skips or errors. Root independently ran: + +```console +.venv/bin/python -m pytest tests/unit/test_capsule_analyzer_diagnostics.py -o addopts= -q -o junit_family=legacy --junitxml=/private/tmp/513-diagnostics-final-green.xml +``` + +Result: 126 passed, one warning, in 7.68 seconds. The warning reports sandbox denial of a pytest cache write, not a skipped or failed case. Workflow SHA-256 was `70d0638343207373c4d4ba5e38b2ae149a34a907e63996c7597b8ead529448cd`. The focused-plus-existing projection suites also passed together: 350 passed, zero skips or failures, one cache-write warning. Three existing unknown-code expectations now require the explicit `unclassified` value; the exact-output helper now expects structured provenance. No existing scenario or assertion was removed. + +The local boundary verifier at `/private/tmp/513-verify-diagnostics-boundary.py` passed. Exactly four existing inline Python snippets changed. All other workflow shell commands, review capture/exit handling, job configuration, prerequisites, pins, budgets, artifact paths, and YAML values match the original head. Module/runtime code and signed payloads are unchanged. + +The new test module's redundant docstring was removed after independent comment review. Test predicates and selectors did not change. The first combined run exposed six stale legacy unknown-code expectations, which were updated to the approved explicit unclassified output before the passing combined run. Its failed output remains `/private/tmp/513-diagnostics-green.txt` only in earlier tool history because the final combined run reused that path; final and preliminary RED records above remain immutable. + +Requirements reconciliation must preserve the honest mixed RED record of 50 regressions and 76 passing controls. Use the existing ledger-supported reconciliation contract, retain both source-bound XML records, and never claim that all 126 tests failed before the change. Local proof cannot grant protected CI authority. + +## Local quality and review gates + +All ten local gate commands completed with exit 0: format, type-check, lint, YAML lint, bundle imports, strict signatures/version checks, contracts, SMART tests, full tests, and the complete staged pre-commit pipeline. Strict signatures verified all seven unchanged module manifests. Contracts passed 28 tests. SMART and full tests each passed 5,851 tests with 77 existing opt-in skips, five baseline warnings, and 95 passing subtests. The full command also passed all 30 host deferral proofs. No signed payload, dependency, registry row, generated artifact, or runtime source changed. + +The capsule component remains explicitly DEFERRED under the previously approved ARM64 Darwin exception. It is not PASS and does not waive the mandatory exact-head Linux matrix or independent signed review. Gate logs/status are under `.audit/513-diagnostics-gate-*`. + +CodeRabbit CLI 0.9.1 completed review of all 12 selected files. Its first review raised one minor missing-GREEN-evidence finding; the actual source-bound passing results above resolve it. The final review completed with zero findings. Independent mapping/shape and comment review also completed; no code-quality suppression was added. + +The actual Requirements consumer reconciled all 126 passing selectors against mapping digest `sha256:fa140be021ab83cfb98f7daf1d862b2733a1ec00c680dd723adbdb9b446f1bf0`, using the existing explicit mixed-history ledger contract. Its result is `verified`, `passing-after-legacy-tdd-ledger`, with no findings. This describes local fixture evidence only. The ledger preserves the 50-failure/76-control RED record and both XML digests. The final source binding is `.audit/513-diagnostics-tested-source.json`; no protected authority is inferred. + +## Hosted verification boundary + +The next signed head must run the unchanged required Linux matrix and independent signed reviewer. Record actual job outcomes and bounded class/side/lifecycle observations in the PR delivery record. Runtime, admission, dependency, budget, enforcement, or acceptance repairs require owner approval after those observations. This implementation does not authorize merge or OpenSpec archival. + +## First exact-head Linux observations and fixture correction + +GitHub verified signed commit `e2b6487679e7ecd06cb8dea7537a473584990f93` as djm81 with the matching noreply email. Run [40139232488](https://github.com/nold-ai/specfact-cli-modules/actions/runs/40139232488) passed signatures, documentation, Requirements, and Python 3.11/3.12/3.13 minimum-core checks. Its Python 3.12 candidate and independent signed review failed. Public log tails are truncated; the local observation record is `.audit/513-diagnostics-linux-e2b64876.json`. + +The candidate projection distinguished 126 new fixture failures with `project_python_option_unsupported`, option `-I`; 31 existing shell-test failures with `FileNotFoundError`; six native skips; and a pre-existing CC16 security test. Its pytest tool error remained explicitly unclassified, and aggregate contract/pytest evidence reported unavailable side metadata. The independent job reached its existing analysis timeout and produced no report, so its prior contract exception and Pylint lifecycle remain unknown. The existing diagnostic replay is not required review authority. + +The new fixture incorrectly assumed isolated-mode support in the sealed project Python wrapper. Its supported protocol deliberately rejects `-I`, `-E`, and `-S` because they disable required runtime attachment. Correct only the fixture subprocess to use the existing supported `python -c` form. All production controller `-I` launches, admission, guards, and security tests remain unchanged. The fixture also uses an explicit pytest alias to remove 14 reproduced Pylint W0621 warnings. The actual adapter then reports zero findings for the new file; all 126 selectors and test assertions remain unchanged and pass locally. + +The fixture correction is an in-scope Linux finding fix, not a runtime compatibility repair. Its second full gate cycle completed with all ten commands at exit 0. SMART and full tests each passed 5,851 tests with 77 existing opt-in skips, five baseline warnings, and 95 passing subtests; full tests also passed the 30 host deferral proofs. The focused suite passed 126 cases and the combined projection suites passed 350 cases. CodeRabbit reviewed the two-file correction and completed with zero findings. The capsule component remains DEFERRED locally; signed exact-head Linux verification is still required. + +Do not infer that missing Bash, native acceptance prerequisites, baseline quality debt, timeout, or unknown analyzer evidence has been resolved. A broader legacy test cleanup proposal was examined only in a temporary copy and left unapplied. The second gate cycle reused the `.audit/513-diagnostics-gate-*` paths; these files now describe that cycle, not immutable first-cycle logs. Original RED and GREEN records remain at the distinct paths documented above. diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/design.md b/openspec/changes/code-review-linux-analyzer-diagnostics/design.md new file mode 100644 index 00000000..106b5906 --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/design.md @@ -0,0 +1,55 @@ +# Design: Preserve provenance at the existing CI output boundary + +## Grounding and data flow + +The controller prepares base/head project descriptors. Sealed analyzers execute with admitted roots and a cleared environment. Their private reports contain analyzer outcomes and findings. Existing inline Python snippets release bounded public diagnostics. That last projection loses analyzer side, exception class, and finding lifecycle information. + +```mermaid +flowchart LR + P[Prepared descriptors] --> A[Sealed analyzer execution] + A --> R[Private review report] + R --> D[Existing CI diagnostic projection] + D --> O[Bounded public observations] + R --> G[Existing required review decision] +``` + +Public observations do not feed back into the required review decision. Candidate source and the independent signed reviewer retain separate identities and evidence. + +## Alternatives and recommendation + +| Approach | Benefit | Limitation and maintenance | Decision | +| --- | --- | --- | --- | +| Reuse existing logs/artifacts unchanged | No code change | Uploaded artifact paths exclude the private reports. Public structured-report output lacks the deciding fields. Repeating unchanged runs adds no evidence. | Insufficient for this failure. | +| Adapt existing inline projections | Reuse current private report access, bounds, allowlists, and trusted workflow ownership | Small duplicated projection sites need shared fixture expectations. | Recommended first unit. | +| Add a standalone diagnostic program | Could centralize projection | Adds a host executable and provenance/dispatch questions for the independent job. | Defer. | +| Provision native tools or replace analyzer execution | Might allow native acceptance execution | Changes signed closure, runtime resources, or acceptance ownership before the separate exceptions are known. | Excluded. | + +## Proposed observations + +Use only existing private report fields and pytest observation records. Validate them at the public-output boundary. Do not import subject Python, run subject helpers, inspect arbitrary host paths, or probe the capsule with extra commands. + +- Analyzer identity comes from the existing finite allowlist. Side is `base`, `head`, or explicitly unavailable. Execution/evidence values come from the existing closed schema. Never infer that head failed from aggregate UNKNOWN. +- CrossHair classes reuse the existing fixed marker-to-enum mapping for timeout, process error, unrecognized output, execution error, and enumerated exception observations. Apply it consistently to readable structured and oversized reports. Never emit exception messages, import names, paths, or tracebacks. +- Pytest uses known fixed diagnostic codes and a small enumerated exception classification. An unrecognized failure emits an explicit unclassified value rather than guessing a cause from its anchor file. +- Pytest producer prefixes distinguish execution failure, evidence-reading failure, and the exact observer/JUnit/process reconciliation failure. Only the two known producer wrappers may be removed before interpreting an existing fixed diagnostic code or errno. Arbitrary wrappers remain unclassified. +- A known native skip detail may emit `native_prerequisites_not_supplied`. Absent or unexpected detail emits no claim about actual prerequisite presence. Do not echo environment values or skip text. +- Finding lifecycle permits only `introduced`, `fixed`, `unchanged`, or `unknown`. Status permits only `open`, `fixed`, or `waived-by-reference`. Emit `blocking` only when its type is exactly Boolean. Missing fields remain unavailable, never implicitly non-blocking. Preserve existing validated public locations. +- Bind observations to the already verified subject and reviewer identities. Do not accept new identity claims from arbitrary report strings or mix candidate and independent output. Public diagnostics remain observations rather than protected assurance. + +Reuse current report-size, iteration, location, and output-count limits. Reject malformed/duplicate-key input where the current parser already does so. Do not loosen any parser to obtain additional data. Raw reports, receipts, logs, environment values, and free-form messages remain runner-private and outside uploaded artifact paths. + +## Test placement and verification + +Create `tests/unit/test_capsule_analyzer_diagnostics.py`. Extract and exercise the actual existing workflow snippets with bounded JSON fixtures using Python, without Bash or capsule launch. This avoids adding tests to the unrelated legacy shell-test file while retaining it in all applicable full verification. + +The existing legacy exact-output expectation requires a narrow update for newly visible structured-report fields. Keep all its scenarios and exact assertions. Do not add cases to that file, suppress its shell tests, or change its runtime dependencies to satisfy sealed analysis. + +Use observable assertions: exact allowed public fields, ordinary/oversized parity, correct side, fixed non-blocking versus active blocking status, unknown handling, and absence of seeded private strings. Include malformed input, duplicate keys, invalid Booleans, bounds, and invalid locations. Verify that diagnostic success or failure cannot replace the captured review exit or its required failing outcome. + +Follow spec, tests, RED evidence, implementation, GREEN evidence. Run strict OpenSpec validation, format, types, lint, YAML, imports, signatures, contracts, SMART/full tests, SpecFact review, and the prescribed independent review. Preserve all pre-existing tests. The Darwin exception may defer only its documented capsule component to blocking exact-head Linux review and must remain DEFERRED. + +## Stop conditions and risks + +The existing Linux exceptions may reveal an actual import, compatibility, collection, or runtime resource defect. Those alternatives are hypotheses, not approved repairs. Stop after the diagnostic unit and present the observed class and side with the smallest supported next action. No timeout, dependency, closure, test selection, or enforcement change is authorized here. + +The main failure modes are leaking private data, presenting base history as active head failure, and promoting diagnostics to authority. Fixed allowlists/bounds, explicit lifecycle/side, and unchanged review exit handling address them. Revert the projection/test unit to roll back. diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/proposal.md b/openspec/changes/code-review-linux-analyzer-diagnostics/proposal.md new file mode 100644 index 00000000..f95e2be6 --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/proposal.md @@ -0,0 +1,56 @@ +# Change: Expose bounded Linux analyzer failure provenance + +## Why + +PR [#524](https://github.com/nold-ai/specfact-cli-modules/pull/524) remains blocked at head `59611f358572aaa3c383c2d2fffcf82fb57ff36c`. The Linux reports establish failed pytest evidence and independent contract uncertainty, but the public projection discards information needed to identify the remaining exceptions. It also displays retained findings without lifecycle and blocking fields needed to determine whether they are fixed or active. + +The investigation establishes the native skip prerequisites and the diagnostic information loss. It does not establish the separate Linux pytest or CrossHair exception. See [the investigation](INVESTIGATION.md). Repairing an assumed exception or adding capsule dependencies now would exceed the evidence. + +## What changes + +Prepare one diagnostic-only implementation unit for owner approval: + +- Extend the existing candidate and independent CI projections with bounded analyzer side and failure-class observations for readable reports, including ordinary structured reports. +- Preserve existing finding lifecycle and explicit blocking fields in the public projection. +- Classify known native-prerequisite skip details using existing pytest observations. Missing details remain unknown. +- Test the actual workflow projection snippets with Python-only fixtures in a new focused test file. Preserve every existing workflow test and required gate. + +The owner approved this diagnostic-only implementation on 2026-10-25, Europe/Berlin. It does not promise green CI. The next decision uses actual exact-head Linux evidence to select a separate, justified repair if one is necessary. + +## Capabilities + +### New capabilities + +- `linux-analyzer-failure-diagnostics`: bounded CI failure provenance with unchanged review authority. + +### Modified capabilities + +None. Existing report schema, analyzer behavior, differential enforcement, runtime admission, and acceptance requirements remain authoritative. + +## Intent and approval boundary + +User-confirmed outcome: a Python developer reviews their own trusted repository without SpecFact dependency conflicts or manual environment repair. The first existing/cold detached Hatch milestone belongs to #513 and change 17. This follow-up improves diagnosis of its mandatory Linux review without introducing customer setup steps. + +The user requested investigation and a minimal scoped OpenSpec change on 2026-10-25, Europe/Berlin, then explicitly approved implementation, all required gates, and exact-head Linux verification. Actual exception repairs, acceptance ownership changes, or capsule resource admission require a further evidence-based scope decision. + +## Dependencies and non-goals + +This modules CI follow-up is tracked under the existing authorized #513 takeover and #524, separate from changes 17 through 20 and #518 baseline smoke-identity maintenance. No paired core runtime change is proposed. Fresh public metadata confirms parent #163, Project SpecFact CLI, owner djm81, required labels, no direct blockers, and unchanged downstream #514/#515 relationships. Reuse the dedicated #513 branch/worktree so exact-head verification observes the original failing subject. No additional issue or duplicate implementation branch is needed for this bounded unit. + +Excluded: host environment or executable pass-through, new mounts, native capsule support, new dependencies, toolchain pins/locks or assets, timeout increases, test deselection/skips/xfail, changed enforcement or verdicts, reviewer replacement, customer reviewer adoption, other managers/platforms, and unrelated cleanup. Do not merge #524 or archive change 17 on diagnostic evidence. + +## Impact and completion + +Implementation files are `.github/workflows/capsule-customer-execution.yml` and `tests/unit/test_capsule_analyzer_diagnostics.py`, plus a narrow update to existing exact-output expectations in `tests/unit/test_capsule_review_projection.py` and this change's evidence. The existing tests remain enabled and retain exact assertions. No signed module payload, manifest, registry row, CLI syntax, report schema, or public documentation permalink changes are planned. If that boundary cannot hold, stop for scope review. + +Completion requires failing-before and passing-after projection tests, unchanged privacy and authority invariants, all applicable repository gates, independent review, and actual exact-head candidate/independent Linux projections. Underlying review failures remain blocking. Strict OpenSpec validation proves artifact structure only. + +Effort is bounded to one projection/test unit and one verification cycle after changed inputs. No measured implementation or monetary estimate is available. If safe diagnostics cannot be obtained within existing report fields, stop with the missing evidence. Rollback reverts the diagnostic unit without altering reviewer decisions. + +## Source tracking + + +- Related issue: [#513](https://github.com/nold-ai/specfact-cli-modules/issues/513). +- Related PR: [#524](https://github.com/nold-ai/specfact-cli-modules/pull/524). +- Tracking: #513, with the owner's bounded diagnostics approval recorded in its public scope. +- Status: diagnostic-only implementation approved; runtime/admission repairs remain unapproved. diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/requirements-evidence.yaml b/openspec/changes/code-review-linux-analyzer-diagnostics/requirements-evidence.yaml new file mode 100644 index 00000000..86b833d7 --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/requirements-evidence.yaml @@ -0,0 +1,1176 @@ +schema_version: '2' +requirements: + openspec:code-review-linux-analyzer-diagnostics:linux-analyzer-failure-diagnostics:bounded-analyzer-failure-provenance: + rationale: Owner-approved diagnostic unit. Exact selectors are authored before production edits; source-bound + RED/GREEN and exact-head Linux evidence remain required. + stakeholder_refs: + - nold-ai/specfact-cli-modules#513 + touchpoints: + - id: linux-analyzer-failure-diagnostics-spec + kind: config_file + locator: openspec/changes/code-review-linux-analyzer-diagnostics/specs/linux-analyzer-failure-diagnostics/spec.md + verification_cases: + - case_id: test-failure-provenance-retains-actual-side-base-false-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[base-False-customer] + - case_id: test-failure-provenance-retains-actual-side-base-false-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[base-False-independent-review] + - case_id: test-failure-provenance-retains-actual-side-base-true-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[base-True-customer] + - case_id: test-failure-provenance-retains-actual-side-base-true-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[base-True-independent-review] + - case_id: test-failure-provenance-retains-actual-side-head-false-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[head-False-customer] + - case_id: test-failure-provenance-retains-actual-side-head-false-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[head-False-independent-review] + - case_id: test-failure-provenance-retains-actual-side-head-true-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[head-True-customer] + - case_id: test-failure-provenance-retains-actual-side-head-true-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[head-True-independent-review] + - case_id: test-failure-provenance-retains-actual-side-unavailable-false-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[unavailable-False-customer] + - case_id: test-failure-provenance-retains-actual-side-unavailable-false-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[unavailable-False-independent-review] + - case_id: test-failure-provenance-retains-actual-side-unavailable-true-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[unavailable-True-customer] + - case_id: test-failure-provenance-retains-actual-side-unavailable-true-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_provenance_retains_actual_side[unavailable-True-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-timeout-false-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[timeout-False-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-timeout-false-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[timeout-False-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-timeout-true-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[timeout-True-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-timeout-true-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[timeout-True-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-process-false-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[process-False-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-process-false-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[process-False-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-process-true-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[process-True-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-process-true-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[process-True-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-unrecognized-false-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[unrecognized-False-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-unrecognized-false-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[unrecognized-False-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-unrecognized-true-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[unrecognized-True-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-unrecognized-true-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[unrecognized-True-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-execute-false-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[execute-False-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-execute-false-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[execute-False-independent-review] + - case_id: test-crosshair-classes-match-structured-and-oversized-execute-true-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[execute-True-customer] + - case_id: test-crosshair-classes-match-structured-and-oversized-execute-true-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_classes_match_structured_and_oversized[execute-True-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-module-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[module-customer] + - case_id: test-crosshair-exception-observations-are-closed-module-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[module-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-import-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[import-customer] + - case_id: test-crosshair-exception-observations-are-closed-import-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[import-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-syntax-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[syntax-customer] + - case_id: test-crosshair-exception-observations-are-closed-syntax-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[syntax-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-permission-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[permission-customer] + - case_id: test-crosshair-exception-observations-are-closed-permission-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[permission-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-type-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[type-customer] + - case_id: test-crosshair-exception-observations-are-closed-type-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[type-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-file-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[file-customer] + - case_id: test-crosshair-exception-observations-are-closed-file-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[file-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-value-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[value-customer] + - case_id: test-crosshair-exception-observations-are-closed-value-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[value-independent-review] + - case_id: test-crosshair-exception-observations-are-closed-parameter-order-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[parameter-order-customer] + - case_id: test-crosshair-exception-observations-are-closed-parameter-order-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_crosshair_exception_observations_are_closed[parameter-order-independent-review] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-unknown-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[unknown-customer] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-unknown-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[unknown-independent-review] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-collection-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[collection-customer] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-collection-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[collection-independent-review] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-file-missing-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[file-missing-customer] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-file-missing-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[file-missing-independent-review] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-permission-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[permission-customer] + - case_id: test-pytest-error-classification-does-not-infer-from-anchor-permission-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_error_classification_does_not_infer_from_anchor[permission-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-workspace-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-workspace-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-workspace-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-workspace-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-storage-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-storage-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-storage-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-storage-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-declaration-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-declaration-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-declaration-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-declaration-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-unknown-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-unknown-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-unknown-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-unknown-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-absent-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-absent-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-skipped-absent-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[skipped-absent-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-workspace-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-workspace-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-workspace-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-workspace-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-storage-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-storage-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-storage-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-storage-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-declaration-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-declaration-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-declaration-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-declaration-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-unknown-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-unknown-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-unknown-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-unknown-independent-review] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-absent-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-absent-customer] + - case_id: test-native-prerequisite-classification-uses-only-recorded-skips-failed-absent-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_native_prerequisite_classification_uses_only_recorded_skips[failed-absent-independent-review] + - case_id: test-recorded-pytest-exceptions-remain-allowlisted-known-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_recorded_pytest_exceptions_remain_allowlisted[known-customer] + - case_id: test-recorded-pytest-exceptions-remain-allowlisted-known-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_recorded_pytest_exceptions_remain_allowlisted[known-independent-review] + - case_id: test-recorded-pytest-exceptions-remain-allowlisted-unknown-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_recorded_pytest_exceptions_remain_allowlisted[unknown-customer] + - case_id: test-recorded-pytest-exceptions-remain-allowlisted-unknown-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_recorded_pytest_exceptions_remain_allowlisted[unknown-independent-review] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-execute-file-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[execute-file-customer] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-execute-file-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[execute-file-independent-review] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-execute-permission-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[execute-permission-customer] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-execute-permission-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[execute-permission-independent-review] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-execute-unknown-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[execute-unknown-customer] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-execute-unknown-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[execute-unknown-independent-review] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-read-collection-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[read-collection-customer] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-read-collection-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[read-collection-independent-review] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-read-unknown-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[read-unknown-customer] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-read-unknown-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[read-unknown-independent-review] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-reconciliation-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[reconciliation-customer] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-reconciliation-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[reconciliation-independent-review] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-arbitrary-wrapper-customer + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[arbitrary-wrapper-customer] + - case_id: test-pytest-producer-failures-retain-fixed-cause-without-private-text-arbitrary-wrapper-independent-review + scenario_id: bounded-analyzer-failure-provenance + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_pytest_producer_failures_retain_fixed_cause_without_private_text[arbitrary-wrapper-independent-review] + ? openspec:code-review-linux-analyzer-diagnostics:linux-analyzer-failure-diagnostics:finding-lifecycle-remains-distinct-from-severity + : rationale: Owner-approved diagnostic unit. Exact selectors are authored before production edits; source-bound + RED/GREEN and exact-head Linux evidence remain required. + stakeholder_refs: + - nold-ai/specfact-cli-modules#513 + touchpoints: + - id: linux-analyzer-failure-diagnostics-spec + kind: config_file + locator: openspec/changes/code-review-linux-analyzer-diagnostics/specs/linux-analyzer-failure-diagnostics/spec.md + verification_cases: + - case_id: test-finding-lifecycle-preserves-authoritative-fields-fixed-fixed-false-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[fixed-fixed-False-customer] + - case_id: test-finding-lifecycle-preserves-authoritative-fields-fixed-fixed-false-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[fixed-fixed-False-independent-review] + - case_id: test-finding-lifecycle-preserves-authoritative-fields-introduced-open-true-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[introduced-open-True-customer] + - case_id: test-finding-lifecycle-preserves-authoritative-fields-introduced-open-true-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[introduced-open-True-independent-review] + - case_id: test-finding-lifecycle-preserves-authoritative-fields-unchanged-open-true-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[unchanged-open-True-customer] + - case_id: test-finding-lifecycle-preserves-authoritative-fields-unchanged-open-true-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[unchanged-open-True-independent-review] + - case_id: test-finding-lifecycle-preserves-authoritative-fields-unknown-waived-by-reference-false-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[unknown-waived-by-reference-False-customer] + - case_id: test-finding-lifecycle-preserves-authoritative-fields-unknown-waived-by-reference-false-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_finding_lifecycle_preserves_authoritative_fields[unknown-waived-by-reference-False-independent-review] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-none-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[None-customer] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-none-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[None-independent-review] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-0-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[0-customer] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-0-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[0-independent-review] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-1-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[1-customer] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-1-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[1-independent-review] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-false-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[false-customer] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-false-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[false-independent-review] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-value4-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[value4-customer] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-value4-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[value4-independent-review] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-value5-customer + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[value5-customer] + - case_id: test-invalid-lifecycle-never-defaults-to-nonblocking-value5-independent-review + scenario_id: finding-lifecycle-remains-distinct-from-severity + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_invalid_lifecycle_never_defaults_to_nonblocking[value5-independent-review] + ? openspec:code-review-linux-analyzer-diagnostics:linux-analyzer-failure-diagnostics:diagnostic-output-preserves-privacy-bounds + : rationale: Owner-approved diagnostic unit. Exact selectors are authored before production edits; source-bound + RED/GREEN and exact-head Linux evidence remain required. + stakeholder_refs: + - nold-ai/specfact-cli-modules#513 + touchpoints: + - id: linux-analyzer-failure-diagnostics-spec + kind: config_file + locator: openspec/changes/code-review-linux-analyzer-diagnostics/specs/linux-analyzer-failure-diagnostics/spec.md + verification_cases: + - case_id: test-failure-projection-rejects-invalid-private-reports-invalid-json-customer + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[invalid-json-customer] + - case_id: test-failure-projection-rejects-invalid-private-reports-invalid-json-independent-review + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[invalid-json-independent-review] + - case_id: test-failure-projection-rejects-invalid-private-reports-duplicate-key-customer + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[duplicate-key-customer] + - case_id: test-failure-projection-rejects-invalid-private-reports-duplicate-key-independent-review + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[duplicate-key-independent-review] + - case_id: test-failure-projection-rejects-invalid-private-reports-invalid-shape-customer + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[invalid-shape-customer] + - case_id: test-failure-projection-rejects-invalid-private-reports-invalid-shape-independent-review + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[invalid-shape-independent-review] + - case_id: test-failure-projection-rejects-invalid-private-reports-over-bound-customer + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[over-bound-customer] + - case_id: test-failure-projection-rejects-invalid-private-reports-over-bound-independent-review + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_projection_rejects_invalid_private_reports[over-bound-independent-review] + - case_id: test-public-location-bounds-and-private-fields-remain-closed-customer + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_public_location_bounds_and_private_fields_remain_closed[customer] + - case_id: test-public-location-bounds-and-private-fields-remain-closed-independent-review + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_public_location_bounds_and_private_fields_remain_closed[independent-review] + - case_id: test-failure-iteration-limits-do-not-reveal-late-private-evidence-customer + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_iteration_limits_do_not_reveal_late_private_evidence[customer] + - case_id: test-failure-iteration-limits-do-not-reveal-late-private-evidence-independent-review + scenario_id: diagnostic-output-preserves-privacy-bounds + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_failure_iteration_limits_do_not_reveal_late_private_evidence[independent-review] + ? openspec:code-review-linux-analyzer-diagnostics:linux-analyzer-failure-diagnostics:diagnostics-cannot-alter-review-authority + : rationale: Owner-approved diagnostic unit. Exact selectors are authored before production edits; source-bound + RED/GREEN and exact-head Linux evidence remain required. + stakeholder_refs: + - nold-ai/specfact-cli-modules#513 + touchpoints: + - id: linux-analyzer-failure-diagnostics-spec + kind: config_file + locator: openspec/changes/code-review-linux-analyzer-diagnostics/specs/linux-analyzer-failure-diagnostics/spec.md + verification_cases: + - case_id: test-diagnostics-preserve-failed-review-capture-and-identity-17-customer + scenario_id: diagnostics-cannot-alter-review-authority + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_diagnostics_preserve_failed_review_capture_and_identity[17-customer] + - case_id: test-diagnostics-preserve-failed-review-capture-and-identity-17-independent-review + scenario_id: diagnostics-cannot-alter-review-authority + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_diagnostics_preserve_failed_review_capture_and_identity[17-independent-review] + - case_id: test-diagnostics-preserve-failed-review-capture-and-identity-124-customer + scenario_id: diagnostics-cannot-alter-review-authority + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_diagnostics_preserve_failed_review_capture_and_identity[124-customer] + - case_id: test-diagnostics-preserve-failed-review-capture-and-identity-124-independent-review + scenario_id: diagnostics-cannot-alter-review-authority + method: test + intent: Execute the actual workflow projection for this exact fixture case. + observable: Public output preserves approved fixed provenance and existing privacy/authority boundaries. Actual + RED/GREEN execution is recorded separately. + selector: + runner: pytest + node_id: tests/unit/test_capsule_analyzer_diagnostics.py::test_diagnostics_preserve_failed_review_capture_and_identity[124-independent-review] diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/specs/linux-analyzer-failure-diagnostics/spec.md b/openspec/changes/code-review-linux-analyzer-diagnostics/specs/linux-analyzer-failure-diagnostics/spec.md new file mode 100644 index 00000000..04ff6b39 --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/specs/linux-analyzer-failure-diagnostics/spec.md @@ -0,0 +1,77 @@ +## ADDED Requirements + +### Requirement: Bounded analyzer failure provenance + +The existing Linux candidate and independent review diagnostic projections SHALL expose available analyzer side and fixed failure-class observations from readable private reports. They SHALL preserve unknown/unavailable values without inferring a fault from a finding location. + +#### Scenario: Structured report preserves failure side and class + +- **GIVEN** a readable structured report contains an incomplete analyzer on one recorded side and a recognized failure marker +- **WHEN** the existing workflow projects the failure +- **THEN** it emits the allowlisted analyzer, actual recorded side, and fixed failure-class observations without private messages or paths. + +#### Scenario: Oversized report uses the same bounded interpretation + +- **GIVEN** a report exceeds the ordinary size threshold but remains within the existing diagnostic bound +- **WHEN** existing validated report parsing succeeds +- **THEN** structured and oversized projections interpret the same provenance fields consistently without increasing any size or output limit. + +#### Scenario: Unknown pytest failure remains unclassified + +- **GIVEN** a pytest tool error has no recognized diagnostic marker +- **WHEN** its public location names the first selected source file +- **THEN** the projection reports an unclassified failure and does not claim that source file caused it or that a particular exception occurred. + +#### Scenario: Native skip classification uses existing observations + +- **GIVEN** an existing pytest observation records a known native-prerequisite skip detail +- **WHEN** the projection classifies that observation +- **THEN** it emits only the fixed prerequisite-not-supplied classification, and missing or unfamiliar details remain unavailable without host inspection or environment pass-through. + +### Requirement: Finding lifecycle remains distinct from severity + +The public projection SHALL retain validated existing finding lifecycle, status, and explicit blocking information. It SHALL NOT recompute differential classifications or turn missing blocking metadata into a non-blocking claim. + +#### Scenario: Base-only fixed warning remains visible and non-blocking + +- **GIVEN** the authoritative report retains a warning with differential state fixed and explicit blocking false +- **WHEN** the workflow emits its validated public location +- **THEN** the projection includes its fixed lifecycle and false blocking value without suppressing the finding or treating it as an active head failure. + +#### Scenario: Active or missing blocking metadata preserves uncertainty + +- **GIVEN** a report contains introduced or unchanged blocking findings, or has missing/invalid lifecycle or blocking metadata +- **WHEN** those findings are projected +- **THEN** valid blocking metadata remains visible, invalid fields are omitted or unavailable, and no new waiver or non-blocking default is created. + +### Requirement: Diagnostic output preserves privacy bounds + +The projection SHALL emit only existing validated locations, finite enums, strict Booleans, and existing bounded counts. It SHALL preserve parser, size, iteration, artifact, and output limits and SHALL treat private report contents as data. + +#### Scenario: Private strings cannot enter public output + +- **GIVEN** report fields contain seeded private paths, exception text, environment values, or arbitrary import names +- **WHEN** the projection reads the report +- **THEN** none of those strings appears in public output or newly uploaded artifacts and no subject-controlled code executes on the host. + +#### Scenario: Malformed or oversized input fails closed + +- **GIVEN** input violates an existing JSON, duplicate-key, size, shape, location, or iteration constraint +- **WHEN** the projection attempts to read it +- **THEN** it preserves existing refusal/bounded behavior and emits no raw fallback content or passing assurance. + +### Requirement: Diagnostics cannot alter review authority + +The diagnostic unit SHALL preserve all existing required jobs, tests, skips-as-failures, analyzer selection, budgets, admission, signatures, and review verdict/exit semantics. Candidate and independent reviewer identities SHALL remain separate and bound to the verified subject. + +#### Scenario: Successful diagnostics leave a failed review blocking + +- **GIVEN** a required review fails with incomplete analyzer evidence +- **WHEN** its diagnostic projection succeeds or itself fails +- **THEN** the captured review failure and required job remain blocking, and diagnostics cannot establish PASS, merge authority, or change completion. + +#### Scenario: Evidence retains subject and reviewer identity + +- **GIVEN** candidate and independent jobs analyze the same exact head using distinct reviewer implementations +- **WHEN** their diagnostic observations are collected +- **THEN** the observations retain those separate verified identities without accepting arbitrary report identity claims or substituting host reproduction for protected Linux evidence. diff --git a/openspec/changes/code-review-linux-analyzer-diagnostics/tasks.md b/openspec/changes/code-review-linux-analyzer-diagnostics/tasks.md new file mode 100644 index 00000000..f7d7092a --- /dev/null +++ b/openspec/changes/code-review-linux-analyzer-diagnostics/tasks.md @@ -0,0 +1,31 @@ +# Tasks + +The owner approved diagnostic-only implementation on 2026-10-25, Europe/Berlin. Planning validation is not runtime evidence. + +## 1. Establish readiness + +- [x] Obtain approval for this diagnostic-only unit and record its scope. +- [x] Refresh public hierarchy metadata, check for an existing follow-up issue, and establish parent, labels, project, blockers, blocked-by relationships, assignee, and current ownership before implementation. Reuse the authorized #513 takeover only within its agreed scope. +- [x] Use the existing dedicated #513 branch/worktree, preserve #524 and other worktrees, and revalidate exact source/reviewer identities and current change order. +- [x] Revalidate this change with `openspec validate code-review-linux-analyzer-diagnostics --strict`. + +## 2. Specify and reproduce + +- [x] Confirm all spec scenarios and convert planning inspection mappings to exact executable selectors before production edits. +- [x] Add focused Python-only tests of actual workflow snippets covering normal/oversized provenance, pytest unknowns/native skip classification, lifecycle/blocking, malformed inputs, redaction, bounds, and unchanged gate exits. Keep all existing tests enabled. +- [x] Capture genuine failing-before evidence against the unchanged projection in `TDD_EVIDENCE.md`. + +## 3. Implement the diagnostic boundary + +- [x] Adapt only existing candidate and independent inline projections. Preserve parser/iteration/output limits, private artifact boundaries, trusted workflow ownership, and captured review exit behavior. +- [x] Emit validated enums and strict Booleans with explicit unavailable/unclassified cases. Do not change analyzers, source/test selection, toolchain, admission, enforcement, verdicts, dependencies, or budgets. +- [x] Run focused tests to GREEN and record source-bound RED/GREEN evidence and Requirements reconciliation. + +## 4. Verify and return evidence + +- [x] Run format, type-check, lint, YAML lint, imports, strict module signature verification, contracts, SMART tests, and full tests. Confirm no signed payload changed; if one must change, stop for scope review rather than bypassing version/signing requirements. +- [x] Retain the documented Darwin DEFERRED condition with mandatory blocking exact-head Linux review, and complete CodeRabbit/independent review before finalization. This local condition is not hosted review PASS. +- [x] Run all existing workflow privacy/shell tests and verify that private strings, paths, raw reports, and receipts cannot enter public output or uploaded artifacts. +- [ ] After authorized signed commit/push, run the unchanged required Linux matrix and independent signed review on the exact new head. Record actual class/side/lifecycle observations and preserve all underlying failures as blocking. +- [ ] Return the observed remaining root causes and a separately justified minimal repair proposal if needed. Do not merge #524 or archive change 17 on this evidence. +- [ ] Archive this follow-up only after its approved implementation merges, using `openspec archive code-review-linux-analyzer-diagnostics`. diff --git a/openspec/changes/code-review-native-hatch-ci/.openspec.yaml b/openspec/changes/code-review-native-hatch-ci/.openspec.yaml new file mode 100644 index 00000000..7ccf7c36 --- /dev/null +++ b/openspec/changes/code-review-native-hatch-ci/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-25 diff --git a/openspec/changes/code-review-native-hatch-ci/TDD_EVIDENCE.md b/openspec/changes/code-review-native-hatch-ci/TDD_EVIDENCE.md new file mode 100644 index 00000000..fbb5695c --- /dev/null +++ b/openspec/changes/code-review-native-hatch-ci/TDD_EVIDENCE.md @@ -0,0 +1,47 @@ +# Native Hatch CI evidence + +Recorded 2026-10-25, Europe/Berlin, against signed base `6b865787bd3dc6e852529be663452b7e1337e6cd`. + +## Scope + +The owner requested continued corrected native implementation. This unit adds hosted execution of the existing first-Hatch acceptance. It does not implement native script expansion, another manager, public reviewer cutover or security-test ownership transfer. No capsule implementation or repair is included. Existing independent review, signing and adverse gate results remain effective. + +## Spec, tests and actual RED + +The spec and sixteen-selector mapping preceded workflow implementation. The actual command was: + +```console +.venv/bin/python -m pytest tests/unit/workflows/test_native_hatch_acceptance.py -o 'addopts=-ra -q --import-mode=importlib' -o junit_family=xunit1 --junitxml=/private/tmp/513-native-ci-red.xml +``` + +All sixteen cases failed. The intended failures were `Mandatory native Hatch acceptance workflow is missing` and `Required native Hatch prerequisite is missing`. This demonstrates absence of the required feature, not sixteen pre-existing bugs in a report parser. There were no collection errors or skipped cases. Source hashes and the RED JUnit digest are in `/private/tmp/513-native-ci-red-binding.json`. + +Independent technical mapping review then accepted the sixteen exact selectors before implementation. Actual `evaluate_mapping` passed at test-authored maturity using `/private/tmp/513-native-ci-mapping-acceptance.json`. The canonical RED reconciliation passed at red maturity against staged tree `ac5948ecceb5266e0a738e5a918a24cf1422ace8`; records are `.audit/513-native-ci-{plan,red-reconciliation}.json`. This technical acceptance grants no product-owner or protected security authority. + +## Implementation and GREEN + +One isolated writer implemented only the reusable native workflow and its required orchestrator prerequisite. The workflow uses exact head checkout, separate controller/project Python installations, pinned native Hatch, the existing controller Hatch environment and the authoritative change-17 selector mapping. Every observed JUnit case must have exactly one mapped selector; missing, extra, duplicate, skipped, failed, erroneous or absent results cannot pass. The existing signature/customer-capsule step is preserved. + +All sixteen workflow checks passed. Combined regression testing also passed all 94 cases covering the new workflow plus existing scope and prerequisite tests. `/private/tmp/513-native-ci-green-regression.xml` is this combined run. The fake pytest producer in workflow checks validates acceptance logic only and does not establish actual project execution. + +The same inline workflow program was separately executed with actual Hatch 1.18.1, controller Python 3.12.13 and project Python 3.11.15. It selected the existing 59 mapped cases, passed all 59 with zero skips in 19.92 seconds and accepted their exact JUnit properties. Existing/cold workflow receipt is `/private/tmp/513-native-ci-real-_plw8u06/proof/receipt.json`; JUnit is `/private/tmp/513-native-ci-real-_plw8u06/junit.xml`. The invocation log and provenance are `.audit/513-native-ci-real.txt` and `.audit/513-native-ci-real-run.json`. This is actual local native execution, not hosted Linux or protected assurance. + +## Archive lifecycle regression + +CodeRabbit found that normal archival of change 17 would remove the active mapping path. The spec and mapping were extended to eighteen cases before the lookup correction. The two new cases genuinely failed in `/private/tmp/513-native-ci-archive-red.xml`. Independent pre-fix acceptance is `/private/tmp/513-native-ci-archive-mapping-acceptance.json`, with mapping digest `sha256:1ece19a51a7df27d35f0f47570d071088dc025225d45b470ca733fc0a576424d`. + +The workflow now prefers the active mapping and otherwise requires exactly one archived source for the same change identity. No real change folder was moved or archived. All eighteen final checks pass in `/private/tmp/513-native-ci-final-green.xml`; the combined retained-scope/prerequisite regression run passes all 96 cases in `/private/tmp/513-native-ci-archive-green-regression.xml`. Independent narrow implementation review accepted the correction in `/private/tmp/513-native-ci-archive-implementation-review.md`. + +The final workflow program also passed all 59 actual native mapped cases with zero skips in 19.66 seconds. Its fresh receipt and JUnit paths are recorded in `.audit/513-native-ci-final-real-run.json`; the execution log is `.audit/513-native-ci-final-real.txt`. An initial outer launch used a Python without PyYAML and stopped before the program ran. The corrected launch used the repository controller Python. This launch error is not a runtime or acceptance failure. + +The final eighteen-case reconciliation uses an explicit incremental legacy-TDD ledger. The initial sixteen-case RED/direct reconciliation remains intact; two later RED cases are not relabeled as initial failing evidence. Final bindings are `.audit/513-native-ci-final-{plan,tdd-ledger,tdd-record,reconciliation,tested-source}.json`. These local artifacts establish engineering evidence only. + +## Delivery gates + +CodeRabbit completed its final ten-file review after the archive fix. Its remaining minor suggestion was to replace the same-repository `./.github/workflows/...` reference with `$/...`. This is a nonblocking style recommendation. [GitHub's reusable-workflow documentation](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows), accessed 2026-10-25, explicitly supports both and states that both select the caller's commit. The existing repository pattern and actionlint-validated `./` reference are retained. No `zizmor` configuration or mandatory gate exists in this checkout. This disposition does not waive a runtime or security finding. + +The final local gate cycle passed formatting, types, lint, YAML, actionlint, bundle import boundaries, all seven strict module signatures, contracts, SMART, full tests and the complete pre-commit pipeline. SMART and full each passed 5,884 tests with 77 existing opt-in skips, five baseline warnings and 95 passing subtests. The pipeline passed its Requirements and 28 contract cases. Logs and actual exit codes are `.audit/513-native-ci-final-gate-{name}.txt` and `.audit/513-native-ci-final-gate-status.json`. + +Only the approved local Darwin capsule component remains explicitly DEFERRED. No successful native receipt turns an old capsule result into PASS. Signed delivery and exact-head hosted status are recorded in the PR after push; hosted native evidence and independent requirements remain separate from these local results. This evidence file does not claim merge-ready status or authorize archive. + +At the prior exact head, the new native scope guard and signatures passed. Linux candidate 3.12 and independent signed review failed; candidate 3.11/3.13 passed. Quality failed at `Required customer capsule matrix did not pass: failure`, before later quality stages, in run `40196995042`. These terminal results are preserved. This unit adds independently required native evidence while leaving the later acceptance/CI replacement to the approved cutover sequence. No old gate is removed, retried or repaired here. diff --git a/openspec/changes/code-review-native-hatch-ci/design.md b/openspec/changes/code-review-native-hatch-ci/design.md new file mode 100644 index 00000000..578808e4 --- /dev/null +++ b/openspec/changes/code-review-native-hatch-ci/design.md @@ -0,0 +1,11 @@ +# Native Hatch acceptance in CI + +The existing change-17 Requirements sidecar owns the executable selector list. The CI program reads its test cases, invokes pytest with executable-first argv and verifies the emitted JUnit selector properties against that exact list. An empty or duplicate mapping, missing report, missing/extra/duplicate observed selector, skipped case, error, failure or nonzero pytest exit cannot pass. Planning inspections are not executable acceptance. + +Use the active change mapping when present. After normal OpenSpec archival, resolve exactly one dated archive directory for the same change identity. Multiple archived candidates are an explicit failure. This preserves one mapping owner through its lifecycle without copying or synchronizing another selector list. + +Use one Ubuntu job and the existing reusable-workflow pattern. Checkout explicitly selects the event's pull-request head, with full history and no persisted credentials. Setup actions provision the two Python installations separately. Native Hatch is installed into the controller's external installation; the target project receives only its declared dependencies through the existing service. The workflow sets all three existing native acceptance inputs before executing the mapping. Its receipt and JUnit live outside the checkout in invocation-owned runner storage. + +The data shapes are the mapped selector list and observed JUnit selector list. Set equality plus equal lengths establishes exact once-only execution. Existing pytest return codes and adverse testcase elements retain their meaning. The inline acceptance program belongs to this workflow; tests execute that same program through a disposable pytest-output producer to exercise outcome validation. The actual hosted run separately proves native execution. + +Add a required native acceptance prerequisite to the existing quality matrix without editing its existing signature/customer-capsule step. The native job has no capsule dependency. Its result does not waive or satisfy independently owned security work. No custom runner framework, manager bridge, product option or new signed module payload is introduced. diff --git a/openspec/changes/code-review-native-hatch-ci/proposal.md b/openspec/changes/code-review-native-hatch-ci/proposal.md new file mode 100644 index 00000000..ff1e41ca --- /dev/null +++ b/openspec/changes/code-review-native-hatch-ci/proposal.md @@ -0,0 +1,25 @@ +# Native CI for the first Hatch milestone + +## Why + +The owner asked to continue the corrected native implementation on 2026-10-25. The existing/cold detached Hatch workflow is already implemented and proved locally. Hosted quality currently waits for the superseded capsule prerequisite and does not configure the native acceptance cell. Add the missing native execution evidence without repairing capsules or waiving an existing gate. + +## What changes + +Run change 17's exact executable test mapping in a mandatory, read-only Linux job against the pull-request head. Provision the already proved Python 3.12.13 controller, Python 3.11.15 project interpreter and Hatch 1.18.1 separately. Use the repository's existing Hatch controller environment and pinned core dependency. Require every mapped test exactly once, with no skips, errors or failures. Preserve the real native receipt and JUnit as ordinary CI artifacts. + +The existing required quality jobs consume this result. Existing module signing and independent security requirements remain unchanged and blocking. This is additional native acceptance, not a transfer of security-test ownership or the replacement CI cutover. No capsule code, runtime API, manager support, public reviewer adoption, release or archive is changed. + +## Capabilities + +### New capabilities +- `native-hatch-ci`: mandatory exact-head execution of the already approved first Hatch acceptance. + +### Modified capabilities +None. + +## Scope and alternatives + +Reuse the mapped tests and existing reusable-workflow/required-prerequisite pattern. More local reruns cannot establish hosted native acceptance. A new runtime adapter or generic CI framework is unnecessary. Moving security cases or removing capsule gates would change acceptance ownership and is excluded. Native Hatch script/context fidelity remains a separate pending runtime unit. + +This continues #513's existing takeover and first-milestone scope. Rollback is a reviewed revert. The check grants no protected `pr_range`, C15 or installed-release authority. diff --git a/openspec/changes/code-review-native-hatch-ci/requirements-evidence.yaml b/openspec/changes/code-review-native-hatch-ci/requirements-evidence.yaml new file mode 100644 index 00000000..36546de5 --- /dev/null +++ b/openspec/changes/code-review-native-hatch-ci/requirements-evidence.yaml @@ -0,0 +1,248 @@ +{ + "schema_version": "2", + "requirements": { + "openspec:code-review-native-hatch-ci:native-hatch-ci:execute-exact-native-hatch-acceptance-in-mandatory-ci": { + "rationale": "Continue the approved first Hatch milestone with native hosted acceptance, preserving independent gates.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-hatch-ci-spec", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-hatch-ci/specs/native-hatch-ci/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "execute-exact-native-hatch-acceptance-in-mandatory-ci-1", + "scenario_id": "execute-exact-native-hatch-acceptance-in-mandatory-ci", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_job_binds_exact_head_and_separate_interpreters" + } + } + ] + }, + "openspec:code-review-native-hatch-ci:native-hatch-ci:incomplete-mapped-execution-cannot-pass": { + "rationale": "Continue the approved first Hatch milestone with native hosted acceptance, preserving independent gates.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-hatch-ci-spec", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-hatch-ci/specs/native-hatch-ci/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "incomplete-mapped-execution-cannot-pass-1", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[complete]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-2", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[skipped]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-3", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[failure]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-4", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[error]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-5", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[missing]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-6", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[duplicate]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-7", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[extra]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-8", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[nonzero]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-9", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[no-report]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-10", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[empty-mapping]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-11", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[duplicate-mapping]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-12", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Keep the single authoritative mapping usable through OpenSpec archival without guessing history.", + "observable": "One archived mapping executes; ambiguous archived sources fail before pytest with an explicit diagnostic.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[archived-mapping]" + } + }, + { + "case_id": "incomplete-mapped-execution-cannot-pass-13", + "scenario_id": "incomplete-mapped-execution-cannot-pass", + "method": "test", + "intent": "Keep the single authoritative mapping usable through OpenSpec archival without guessing history.", + "observable": "One archived mapping executes; ambiguous archived sources fail before pytest with an explicit diagnostic.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_accepts_only_complete_exact_mapping[ambiguous-archive]" + } + } + ] + }, + "openspec:code-review-native-hatch-ci:native-hatch-ci:preserve-existing-independently-owned-gates": { + "rationale": "Continue the approved first Hatch milestone with native hosted acceptance, preserving independent gates.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-hatch-ci-spec", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-hatch-ci/specs/native-hatch-ci/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "preserve-existing-independently-owned-gates-1", + "scenario_id": "preserve-existing-independently-owned-gates", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_result_blocks_quality_without_replacing_security[success-0]" + } + }, + { + "case_id": "preserve-existing-independently-owned-gates-2", + "scenario_id": "preserve-existing-independently-owned-gates", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_result_blocks_quality_without_replacing_security[failure-1]" + } + }, + { + "case_id": "preserve-existing-independently-owned-gates-3", + "scenario_id": "preserve-existing-independently-owned-gates", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_result_blocks_quality_without_replacing_security[cancelled-1]" + } + }, + { + "case_id": "preserve-existing-independently-owned-gates-4", + "scenario_id": "preserve-existing-independently-owned-gates", + "method": "test", + "intent": "Exercise the actual native workflow contract or its acceptance program.", + "observable": "Exact native inputs and mapped success are required; adverse or incomplete evidence fails without replacing independent gates.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/workflows/test_native_hatch_acceptance.py::test_native_hatch_result_blocks_quality_without_replacing_security[skipped-1]" + } + } + ] + } + } +} diff --git a/openspec/changes/code-review-native-hatch-ci/specs/native-hatch-ci/spec.md b/openspec/changes/code-review-native-hatch-ci/specs/native-hatch-ci/spec.md new file mode 100644 index 00000000..755e9407 --- /dev/null +++ b/openspec/changes/code-review-native-hatch-ci/specs/native-hatch-ci/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: Execute exact native Hatch acceptance in mandatory CI +CI SHALL execute change 17's mapped pytest cases on Linux against the event-selected pull-request head, using separate Python 3.12.13 controller and Python 3.11.15 project installations and native Hatch 1.18.1. It SHALL configure the native acceptance cell and retain JUnit and the native workflow receipt outside the checkout. The job SHALL use read-only repository permissions without persisted credentials and SHALL run independently of capsule jobs. + +#### Scenario: Native acceptance has explicit inputs and source +- **WHEN** a pull request schedules verification +- **THEN** the native job checks out its exact head and supplies every native proof prerequisite with the two independent Python installations + +### Requirement: Incomplete mapped execution cannot pass +The native job SHALL derive executable pytest selectors from the existing change-17 mapping and require each selector exactly once in JUnit. Empty or duplicate mappings, nonzero pytest exits, missing reports, absent or additional selectors, duplicate results, skips, errors and failures SHALL be nonaccepting. Inspection-only planning entries SHALL not be represented as executed tests. + +The mapping SHALL remain usable after normal OpenSpec archival. Prefer the active mapping; when it is absent, require exactly one archived mapping for that change identity. An ambiguous archived source SHALL fail rather than selecting an arbitrary history entry. + +#### Scenario: Mapping survives OpenSpec archival +- **WHEN** the active change mapping has moved into one dated OpenSpec archive directory +- **THEN** native acceptance executes the same mapped cases from that archive +- **AND** multiple matching archived mappings are rejected with an explicit diagnostic + +#### Scenario: Complete mapped execution passes +- **WHEN** every mapped selector appears once with a passing result +- **THEN** acceptance succeeds and pytest receives precisely the mapped selectors + +#### Scenario: Adverse or incomplete execution fails +- **WHEN** pytest fails or the report is absent, skipped, failed, erroneous, duplicated or does not match the mapping +- **THEN** native acceptance fails + +### Requirement: Preserve existing independently owned gates +The existing required quality matrix SHALL require the native result while preserving module-signature and customer-capsule prerequisites. This additional native acceptance SHALL not promote local evidence to protected review, replace independent security or claim the later public reviewer cutover. + +#### Scenario: Native acceptance is a required prerequisite +- **WHEN** the native job does not pass +- **THEN** the quality prerequisite refuses and the existing signature and independent-review requirements remain present diff --git a/openspec/changes/code-review-native-hatch-ci/tasks.md b/openspec/changes/code-review-native-hatch-ci/tasks.md new file mode 100644 index 00000000..f6bb6737 --- /dev/null +++ b/openspec/changes/code-review-native-hatch-ci/tasks.md @@ -0,0 +1,10 @@ +# Tasks + +- [x] Reuse the confirmed native direction, #513 ownership and first Hatch scope. +- [x] Specify mandatory native acceptance while preserving independent gates. +- [x] Map executable cases and record actual RED before workflow implementation. +- [x] Reuse the existing workflow pattern and mapped native tests; verify GREEN. +- [x] Run native acceptance, required local gates and engineering reviews. +- [ ] Deliver the signed commit and verify its identity on GitHub. +- [ ] Verify exact-head Linux native acceptance; preserve any independent failures. +- [ ] Archive only after actual completion and merge through OpenSpec CLI. diff --git a/openspec/changes/code-review-native-migration-guard/.openspec.yaml b/openspec/changes/code-review-native-migration-guard/.openspec.yaml new file mode 100644 index 00000000..7ccf7c36 --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-25 diff --git a/openspec/changes/code-review-native-migration-guard/CI_TRANSITION.md b/openspec/changes/code-review-native-migration-guard/CI_TRANSITION.md new file mode 100644 index 00000000..b0cb0064 --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/CI_TRANSITION.md @@ -0,0 +1,30 @@ +# Required CI transition for native review + +## Historical dependency before the authorized CI transition + +`.github/workflows/pr-orchestrator.yml` makes `quality` depend on `customer-capsules`. Its first prerequisite step rejects a non-successful capsule result before checkout, formatting, types, lint, contracts or tests execute. The approved Darwin local deferral also requires the indexed candidate and independent signed capsule jobs. That historical contract blocked merge before the maintainer-authorized transition below. + +This is a migration dependency, not evidence that the new native execution owner needs capsule resources. The approved execution direction remains trusted local native execution. Do not repair capsule tooling, native dependencies, admission or diagnostics to clear it. + +## Transition owner and acceptance + +Changes 17 and 18 prove native project execution and project-selected checks. Change 19 prepares the single native runner, local report and exact signed cutover candidate, deleting both obsolete execution routes. Paired core adoption consumes that exact candidate. Change 20 proves installations and replaces capsule-only CI before coordinated stable publication. Preserve this order; this correction does not advance ordinary reviewer cutover into the first Hatch milestone. + +At that accepted transition: + +1. Replace the capsule-only prerequisite with required exact-head native end-to-end acceptance. Keep missing prerequisites, skipped required cases, failed assertions, incomplete evidence and timeouts nonaccepting. +2. Preserve independent security review and cryptographic module verification as separately owned requirements. A host receipt or local schema-2.0 result cannot silently satisfy protected `pr_range`, core #680 or C15 contracts. +3. Replace the local capsule-specific deferral with the actual accepted native review command and evidence contract. Do not retain a permanent backend selector. +4. Retire capsule-only build/customer workflows and obsolete implementation in the owning cutover/distribution units. Preserve useful historical evidence and source-bound findings without continuing their implementation queue. + +The complete native acceptance and independent-security consumer must exist and pass before an old mandatory gate is removed. If that transition cannot preserve a protected contract, record the exact unresolved interface; do not manufacture compatibility fields or call a local result protected assurance. + +## Original migration-guard scope, superseded for CI scheduling + +The new lightweight native scope job runs independently of capsule execution. The existing required quality matrix consumes its successful result. The same checker runs in the existing local boundary stage. This enforces the prohibition on new capsule work without replacing any runtime/security verdict or changing test ownership. Under that original scope, failing capsule results remained visible and blocking. The separately authorized native CI transition below replaces this prerequisite only after complete native evidence passes. There is no new choice between capsule expansion and native execution. + +## Maintainer-authorized CI exception, 2026-10-25 + +The maintainer subsequently authorized the CI/security transition before merging PR #524. This supersedes the earlier scheduling restriction for CI only. `code-review-native-pr-ci-transition` owns the complete independently installed signed native reviewer, raw completeness barrier, native test/coverage evidence and exact-head prerequisites. Public runtime and distribution changes remain in 17 → 18 → 19 → paired core → 20. No capsule repair or protected-report compatibility is authorized. + +The proposed prerequisite replacement remains unaccepted until all required local gates, independent review and exact-head Linux native evidence pass. A failing native replacement continues to block quality and merge. No historical capsule failure is relabeled successful. diff --git a/openspec/changes/code-review-native-migration-guard/TDD_EVIDENCE.md b/openspec/changes/code-review-native-migration-guard/TDD_EVIDENCE.md new file mode 100644 index 00000000..c5eb4999 --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/TDD_EVIDENCE.md @@ -0,0 +1,55 @@ +# Native migration correction evidence + +Observed 2026-10-25, Europe/Berlin. Baseline is signed head `23967ee8dc22545935427cd737b418e5b21198e5`. The owner explicitly requested native Hatch work and an enforced correction against recurring capsule repairs. + +## RED before implementation + +The spec and exact eleven-selector mapping preceded the authoritative run: + +```console +.venv/bin/python -m pytest tests/unit/scripts/test_native_review_scope.py -q -o addopts= -o junit_family=legacy -o cache_dir=/private/tmp/513-native-guard-cache --junitxml=/private/tmp/513-native-guard-final-red.xml +``` + +Result: 8 failed, 3 passed. The old mandatory boundary command returned `0` and `Import boundary check passed.` when the new rejection cases expected exit `1`. The actual earlier workflow content from `e2b6487679e7ecd06cb8dea7537a473584990f93` was also accepted instead of rejected. The independent native CI workflow was absent. Three preservation controls already passed. No claim is made that all eleven cases were RED. Source/JUnit hashes are in `/private/tmp/513-native-guard-red-binding.json`; the preliminary run is separately retained under `/private/tmp/513-native-guard-red.*`. + +## Incremental required-CI proof + +After the first checker GREEN, the CI contract was strengthened to require consumption by the existing quality gate rather than an optional isolated job. Before that wiring, its focused check failed with `AssertionError: Required native scope prerequisite is missing` and the actual mismatch `assert 'native-scope' in {...}`. The earlier exploratory failure was a KeyError and is not substituted for this assertion mismatch. Records are `/private/tmp/513-native-guard-ci-dependency-{red,final-red}.txt`. + +The required quality matrix now depends on the reusable native scope job and checks its result explicitly. The native scope job itself has no capsule prerequisite and read-only repository permissions. Existing signing and capsule/security prerequisites remain unchanged. Test assertions were strengthened, not weakened; the initial and later test source versions are not represented as identical. + +## GREEN and historical rejection + +The initial focused suite passed all 11 cases, zero skips. JUnit is `/private/tmp/513-native-guard-final-green.xml`. The existing `scripts/check-bundle-imports.py` entry point also passed and executed the new checker. + +The read-only historical command: + +```console +.venv/bin/python scripts/check_native_review_scope.py --base-ref 59611f358572aaa3c383c2d2fffcf82fb57ff36c --head-ref e2b6487679e7ecd06cb8dea7537a473584990f93 +``` + +returned exit 1 and rejected the actual capsule workflow and projection-test edits. Its diagnostic directs work to `packages/specfact-codebase/src/specfact_codebase/project_runtime.py` and the established 17 -> 18 -> 19 -> paired core -> 20 order. It reads Git data only and executes no historical subject code. The default command accepts the current native-only change. Existing classified files can remain unchanged or be deleted for retirement; additions and edits fail. + +## Review corrections + +Independent review reproduced option-shaped Git revisions writing an output file. CodeRabbit also identified policy-only weakening by moving the freeze baseline or removing a classified path. Four focused cases failed before correction with `Native review scope check passed` and exit `0` where exit `1` was required. Records are `/private/tmp/513-native-guard-review-red.{txt,xml}`. Resolve revisions to commit identities before diff and bind reviewed policy bytes by SHA-256. This protects against accidental policy-only bypass, not a hostile candidate rewriting the checker itself. + +After those corrections, 15 guard cases and 63 unchanged existing prerequisite cases passed together, 78 passed with zero skips, in `/private/tmp/513-native-guard-review-green.xml`. The first broad SMART run had exposed two existing prerequisite regressions from placing the native variable in the old step. Moving the new native prerequisite into its own step preserved the original signature/customer-capsule behavior without editing frozen tests. + +CodeRabbit subsequently found that fingerprint rejection masked three deeper input-validation cases. The test fixture now explicitly approves its mutated test policy for those cases, while the separate weakening cases retain the original fingerprint. It also required the mapped historical case to invoke the actual two-commit comparison, rather than relying on the separately recorded manual comparison and a copied historical file. These are later test refinements, not identical bytes to original RED. The final exact 15-selector run is `/private/tmp/513-native-guard-final-15-green-v3.xml`. The actual-time independent mapping acceptance is `/private/tmp/513-native-guard-final-mapping-acceptance.json`; it is post-implementation test-design evidence, not pre-code approval or protected security authority. + +## Native Hatch acceptance + +Actual Hatch 1.18.1 and project Python 3.11.15 were verified before the run. With the Python 3.12.13 controller, a fresh external workspace and the existing native acceptance environment inputs, both project-runtime unit and integration modules passed: 56 passed, zero skipped, in 19.41 seconds. The proof executed existing and cold environments and preserved the original fixture identity. Records are `/private/tmp/513-native-ratchet-acceptance.{txt,xml}` and `/private/tmp/513-native-ratchet-proof-20261025-a/receipt.json`. + +The other three selectors in change 17's existing 59-selector mapping are unchanged deferral/security guards. They passed separately, 3 passed, zero skipped, in 0.93 seconds; record `/private/tmp/513-native-retained-security.xml`. This is two executions, not a fabricated single 59-case run. No capsule implementation or repair was performed. + +## Remaining gates + +Final local format, type, lint, YAML, import boundaries, all seven strict module signatures and contracts passed. SMART and full testing each passed 5,866 tests with 77 existing opt-in skips, five baseline warnings and 95 passing subtests. Logs are `.audit/513-native-guard-final-gate-*`. The complete local pipeline passed its executable stages, including Requirements and 28 contract tests; only the approved local capsule review is explicitly DEFERRED, never PASS. Full-suite evidence precedes the final historical-case test refinement; the final 15-selector focused run verifies that test-only refinement without repeating the unchanged runtime suite. + +The canonical Requirements reconciliation for the current mapping reports verified with `passing-after-legacy-tdd-ledger`, not a fabricated uniform RED proof. `.audit/513-native-guard-{plan,tdd-ledger,tdd-record,reconciliation,tested-source}.json` binds mixed historical evidence, actual-time independent mapping acceptance, final JUnit and staged-tree source bytes. These are local proof artifacts and grant no protected CI authority. + +CodeRabbit CLI 0.9.1 completed full fifteen-file review. Earlier major/minor findings were fixed as described above. The final pass returned two minor findings. The spec now states the fingerprint trust limit explicitly. The proposed staged-file bypass was dismissed after the actual installed `pre_commit.staged_files_only` rejected that repair in a disposable repository and restored the working tree; committed CI also checks the committed tree. Standalone working-tree mode retains that documented limitation. Detailed dispositions and proof are `.audit/513-native-guard-review-dispositions.md` and `.audit/513-native-guard-staged-boundary.txt`. No unresolved actionable local engineering finding is claimed, and this disposition does not turn the separate protected security review into PASS. + +The native scope check does not turn existing failures into PASS. The legacy CI transition is specified in `CI_TRANSITION.md`; no old security gate is bypassed by this unit. Independent protected review, exact-head CI and signed delivery remain separate prerequisites. No merge, publication, complete change-17 claim or archive is authorized. diff --git a/openspec/changes/code-review-native-migration-guard/design.md b/openspec/changes/code-review-native-migration-guard/design.md new file mode 100644 index 00000000..f07c9cea --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/design.md @@ -0,0 +1,11 @@ +# Native-only scope ratchet + +One JSON policy owns the execution direction, audit freeze revision, classified legacy path patterns and committed baseline mode/blob manifest. Default checking compares current classified files using Git checkout normalization and current index modes; it does not need the old feature commit after squash integration. Modified or added classified paths fail; removed paths are allowed. Working-tree checks include untracked classified paths. Historical read-only comparisons use explicit base/head revisions and Git name/status records with NUL separators and no rename inference. + +The checker pins the reviewed policy bytes by SHA-256. A policy-only edit cannot move the baseline or remove a classified path. Changing the fingerprint is a change to the enforcement code and requires independent review like any other guard change. This binding prevents accidental configuration bypass; it does not authenticate a hostile candidate that also rewrites its checker. Resolve both comparison revisions with `git rev-parse --verify --end-of-options` before invoking Git diff. Only resolved commit identities enter that command. + +Keep the policy and checker outside signed module payloads. Invoke the checker from the existing boundary command so pre-commit cannot silently omit it. A small separate CI workflow invokes the same Python command with complete Git history, read-only repository permissions and no capsule prerequisite. Existing capsule workflows and their failures remain untouched. + +The ratchet deliberately freezes legacy surfaces rather than trying to infer whether an edit is a worthwhile capsule repair. Future approved native cutover may remove those files. If useful native code needs a currently frozen location, revise the migration ownership explicitly rather than adding an undocumented exception. + +This is a regression guard against known scope drift, not hostile-code containment or proof of all possible future implementation semantics. The owner chose native execution already. No backend choice or runtime framework is introduced. diff --git a/openspec/changes/code-review-native-migration-guard/proposal.md b/openspec/changes/code-review-native-migration-guard/proposal.md new file mode 100644 index 00000000..8ee0eab3 --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/proposal.md @@ -0,0 +1,29 @@ +# Enforce the approved native review migration + +## Why + +The owner approved trusted local native execution on 2026-10-16. During #513, the agent repeatedly returned to capsule diagnostic repairs and then offered capsule expansion as an option. The owner corrected this again on 2026-10-25 and explicitly requested an enforced correction, OpenSpec/TDD, the native Hatch milestone and identification of the CI transition. Existing prose did not prevent this regression. + +## What changes + +Add a native-only scope ratchet to the existing local import-boundary check and a separate lightweight CI job. A machine-readable policy freezes known legacy capsule implementation, tooling, workflow and test surfaces at signed head `23967ee8dc22545935427cd737b418e5b21198e5`. Existing content and retirement deletions remain allowed; additions and edits fail with an actionable native-owner message. The check also rejects an alternative execution direction. Prove rejection against an actual earlier capsule diagnostic commit. + +Re-run the existing native Hatch acceptance through the Codebase project boundary. Record its actual result and the exact legacy CI dependency that requires an approved migration transition. Preserve signing, independent security and all adverse gate outcomes. Do not repair capsules, reroute the 37 legacy-sensitive tests or bring forward ordinary reviewer cutover. + +## Capabilities + +### New capabilities +- `native-review-migration-scope`: enforce native-only implementation ownership while retaining historical capsule evidence and retirement. + +### Modified capabilities +None. This unit does not grant protected review authority or change customer runtime behavior. + +## Scope and alternatives + +Reuse the existing mandatory local boundary stage and add one small standard-library checker with one policy owner. More prose alone failed. Removing the old runtime now would prematurely implement changes 19/20. A capsule repair or a second backend contradicts the confirmed direction. A narrow ratchet is the smallest enforceable correction before cutover. + +User and outcome remain #513's trusted developer-owned repository and first existing/cold detached Hatch workflow, with customer, controller and analyzer dependencies separate. The current task authorizes this governance correction; it does not authorize a new hostile-code runtime, other managers or publication. Existing #513 ownership/takeover is reused. Parent #163, project/labels and downstream #514/#515 remain unchanged. + +No signed module payload changes are planned. Completion requires genuine RED before checker implementation, GREEN for rejection and preservation controls, a real historical rejection, the same checker locally/CI, native workflow execution and applicable gates/review. The ratchet prevents changes on its classified surfaces; it cannot prove that an arbitrary renamed implementation or agent prose is correct. Independent review remains required. + +Rollback is a reviewed revert, never a force push or silent gate bypass. No merge or archive is authorized. diff --git a/openspec/changes/code-review-native-migration-guard/requirements-evidence.yaml b/openspec/changes/code-review-native-migration-guard/requirements-evidence.yaml new file mode 100644 index 00000000..38638b28 --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/requirements-evidence.yaml @@ -0,0 +1,215 @@ +{ + "schema_version": "2", + "requirements": { + "openspec:code-review-native-migration-guard:native-review-migration-scope:enforce-native-only-implementation-scope": { + "rationale": "Owner explicitly requested enforcement of the settled native migration. Exact case results and historical rejection are required; planning is not acceptance.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-migration-spec", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-migration-guard/specs/native-review-migration-scope/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "enforce-native-only-implementation-scope-1", + "scenario_id": "enforce-native-only-implementation-scope", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_capsule_changes[workflow]" + } + }, + { + "case_id": "enforce-native-only-implementation-scope-2", + "scenario_id": "enforce-native-only-implementation-scope", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_capsule_changes[runtime]" + } + }, + { + "case_id": "enforce-native-only-implementation-scope-3", + "scenario_id": "enforce-native-only-implementation-scope", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_capsule_changes[test]" + } + }, + { + "case_id": "enforce-native-only-implementation-scope-4", + "scenario_id": "enforce-native-only-implementation-scope", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_capsule_changes[untracked]" + } + }, + { + "case_id": "enforce-native-only-implementation-scope-5", + "scenario_id": "enforce-native-only-implementation-scope", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_preserves_native_work_and_retirement[unchanged]" + } + }, + { + "case_id": "enforce-native-only-implementation-scope-6", + "scenario_id": "enforce-native-only-implementation-scope", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_preserves_native_work_and_retirement[native]" + } + }, + { + "case_id": "enforce-native-only-implementation-scope-7", + "scenario_id": "enforce-native-only-implementation-scope", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_preserves_native_work_and_retirement[retirement]" + } + } + ] + }, + "openspec:code-review-native-migration-guard:native-review-migration-scope:fail-closed-on-invalid-migration-policy": { + "rationale": "Owner explicitly requested enforcement of the settled native migration. Exact case results and historical rejection are required; planning is not acceptance.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-migration-spec", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-migration-guard/specs/native-review-migration-scope/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "fail-closed-on-invalid-migration-policy-1", + "scenario_id": "fail-closed-on-invalid-migration-policy", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_invalid_policy[backend]" + } + }, + { + "case_id": "fail-closed-on-invalid-migration-policy-2", + "scenario_id": "fail-closed-on-invalid-migration-policy", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_invalid_policy[revision]" + } + }, + { + "case_id": "fail-closed-on-invalid-migration-policy-3", + "scenario_id": "fail-closed-on-invalid-migration-policy", + "method": "test", + "intent": "Reject configuration weakening and option-shaped revisions at the checker boundary.", + "observable": "The command fails and no unexpected output file is created.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_policy_weakening[baseline]" + } + }, + { + "case_id": "fail-closed-on-invalid-migration-policy-4", + "scenario_id": "fail-closed-on-invalid-migration-policy", + "method": "test", + "intent": "Reject configuration weakening and option-shaped revisions at the checker boundary.", + "observable": "The command fails and no unexpected output file is created.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_policy_weakening[paths]" + } + }, + { + "case_id": "fail-closed-on-invalid-migration-policy-5", + "scenario_id": "fail-closed-on-invalid-migration-policy", + "method": "test", + "intent": "Reject configuration weakening and option-shaped revisions at the checker boundary.", + "observable": "The command fails and no unexpected output file is created.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_revision_options_cannot_write_files[policy]" + } + }, + { + "case_id": "fail-closed-on-invalid-migration-policy-6", + "scenario_id": "fail-closed-on-invalid-migration-policy", + "method": "test", + "intent": "Reject configuration weakening and option-shaped revisions at the checker boundary.", + "observable": "The command fails and no unexpected output file is created.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_revision_options_cannot_write_files[head]" + } + } + ] + }, + "openspec:code-review-native-migration-guard:native-review-migration-scope:preserve-independent-gates-and-test-the-correction-on-real-history": { + "rationale": "Owner explicitly requested enforcement of the settled native migration. Exact case results and historical rejection are required; planning is not acceptance.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "native-migration-spec", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-migration-guard/specs/native-review-migration-scope/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "preserve-independent-gates-and-test-the-correction-on-real-history-1", + "scenario_id": "preserve-independent-gates-and-test-the-correction-on-real-history", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_boundary_rejects_actual_historical_capsule_repair" + } + }, + { + "case_id": "preserve-independent-gates-and-test-the-correction-on-real-history-2", + "scenario_id": "preserve-independent-gates-and-test-the-correction-on-real-history", + "method": "test", + "intent": "Exercise the actual mandatory boundary command or its independent CI contract.", + "observable": "Classified capsule changes and invalid policy fail; native preservation controls and effective independent gating remain explicit.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_review_scope.py::test_native_scope_ci_is_independent_and_read_only" + } + } + ] + } + } +} diff --git a/openspec/changes/code-review-native-migration-guard/specs/native-review-migration-scope/spec.md b/openspec/changes/code-review-native-migration-guard/specs/native-review-migration-scope/spec.md new file mode 100644 index 00000000..ae102af9 --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/specs/native-review-migration-scope/spec.md @@ -0,0 +1,56 @@ +## ADDED Requirements + +### Requirement: Enforce native-only implementation scope +The repository SHALL reject additions or edits on classified legacy capsule surfaces after the recorded freeze revision, while allowing unchanged historical content and retirement deletions. The diagnostic SHALL name the native execution owner and approved migration order. + +#### Scenario: Reject a capsule workflow repair +- **WHEN** an agent changes a frozen capsule workflow +- **THEN** the mandatory boundary check fails and directs work to native execution + +#### Scenario: Reject a capsule runtime repair +- **WHEN** an agent changes a frozen capsule runtime file +- **THEN** the boundary check fails + +#### Scenario: Reject capsule test expansion +- **WHEN** an agent changes a frozen capsule test +- **THEN** the boundary check fails + +#### Scenario: Reject a new capsule file +- **WHEN** a new tracked or untracked file is added under a classified legacy surface +- **THEN** the boundary check fails + +#### Scenario: Preserve native work and retirement +- **WHEN** only native-owned code changes or a legacy file is deleted +- **THEN** the scope check accepts that change without claiming runtime acceptance + +### Requirement: Fail closed on invalid migration policy +The checker SHALL accept only the established trusted-local-native execution direction and the committed reviewed baseline manifest. The freeze revision SHALL remain audit metadata rather than a default runtime dependency. An invalid policy or unavailable explicitly requested historical revision SHALL fail with an actionable diagnostic. + +The policy SHALL be bound to the reviewed fingerprint in the checker. A policy-only change that alters its baseline or removes a classified surface SHALL fail rather than silently authorizing capsule work. This binding does not authenticate a candidate that also rewrites the checker and its fingerprint. Revision arguments SHALL resolve to commit identities before comparison and SHALL never be interpreted as Git options. + +#### Scenario: Reject policy weakening +- **WHEN** an agent advances the freeze revision to include a repair or removes a classified path +- **THEN** the mandatory boundary check rejects the altered policy + +#### Scenario: Reject option-shaped revisions +- **WHEN** a policy or historical revision argument contains a Git option +- **THEN** the checker fails without creating an output file + +#### Scenario: Reject an alternative backend +- **WHEN** policy selects capsule execution +- **THEN** the boundary check fails rather than reopening the architecture decision + +#### Scenario: Reject an unavailable historical baseline +- **WHEN** an explicitly requested historical freeze revision is unavailable +- **THEN** the boundary check fails and identifies the missing revision + +### Requirement: Preserve independent gates and test the correction on real history +The same scope checker SHALL run through local mandatory checks and an independent credential-free CI job. The unit SHALL demonstrate rejection of a real historical capsule repair. It SHALL preserve existing signing, independent security and nonpassing legacy review outcomes. + +#### Scenario: Reject a historical recurrence +- **WHEN** the checker compares the pre-diagnostics revision with the actual capsule-diagnostics commit +- **THEN** it rejects the classified workflow edit without executing historical subject code + +#### Scenario: Run the native scope gate independently +- **WHEN** a pull request changes repository implementation +- **THEN** CI runs the scope check without depending on capsule review success or granting write credentials, and the existing required quality matrix depends on its successful outcome diff --git a/openspec/changes/code-review-native-migration-guard/tasks.md b/openspec/changes/code-review-native-migration-guard/tasks.md new file mode 100644 index 00000000..d56bca7b --- /dev/null +++ b/openspec/changes/code-review-native-migration-guard/tasks.md @@ -0,0 +1,10 @@ +# Tasks + +- [x] Reuse the explicit native-only decision, #513 takeover and current worktree. +- [x] Specify the ratchet and preserved security/signing boundaries before tests. +- [x] Add executable cases and Requirements mappings; capture intended RED against the existing boundary command. +- [x] Implement one policy/checker and local/CI wiring; capture GREEN. +- [x] Reject the actual earlier diagnostic commit without executing historical subject code. +- [x] Run the native Hatch acceptance, preserve customer identity/inventory evidence and identify the required CI transition. +- [ ] Run applicable gates, independent review and signed delivery without capsule repairs. +- [ ] Archive only after approved completion and merge, using OpenSpec CLI. diff --git a/openspec/changes/code-review-native-pr-ci-transition/.openspec.yaml b/openspec/changes/code-review-native-pr-ci-transition/.openspec.yaml new file mode 100644 index 00000000..7ccf7c36 --- /dev/null +++ b/openspec/changes/code-review-native-pr-ci-transition/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-25 diff --git a/openspec/changes/code-review-native-pr-ci-transition/TDD_EVIDENCE.md b/openspec/changes/code-review-native-pr-ci-transition/TDD_EVIDENCE.md new file mode 100644 index 00000000..5bd83209 --- /dev/null +++ b/openspec/changes/code-review-native-pr-ci-transition/TDD_EVIDENCE.md @@ -0,0 +1,97 @@ +# Native PR CI transition evidence + +## Authorization and scope + +The maintainer explicitly authorized the CI/security transition before PR #524 merge on 2026-10-25, Europe/Berlin. Capsule repairs remain excluded. This supersedes the earlier defer-cutover decision only for CI. Customer runtime delivery remains 17 → 18 → 19 → paired core → 20. No public dispatcher, runtime admission, protected report, release or issue closure is delivered here. + +## Actual failing-before and passing-after evidence + +After the specification and interface sketch, the first meaningful focused run produced 13 failures and one passing control. Negative receipt cases failed with `DID NOT RAISE`, demonstrating the missing completeness barrier. An earlier import-only draft is retained locally but excluded from behavior proof. Later independent-review, deferral, environment, source-identity, native-import, test-selection, skipped-test, selected-file and interpreter-path regressions each preceded their respective corrections. The final mapping has 24 exact selectors; this is incremental TDD, not a claim of uniform initial RED. All 24 pass with exact selector properties and no failures, errors, skips or new pytest warnings. + +The actual signed native engine initially returned exit zero despite CrossHair UNKNOWN. The CI owner now checks raw completeness before noise or changed-line projection. A separate real failure imported the hyphenated boundary script through CrossHair and raised BeartypeDecorHintForwardRefException. Removing its postponed annotations preserved the Python 3.11+ type meanings and passed the same load_file regression. No analyzer error is accepted through changed enforcement. + +The detached full-suite experiment completed 5,993 cases without assertion failures, but was rejected because selecting `tests` alongside the proof file omitted that explicit proof. The final owner uses a deduplicated explicit inventory of changed test files and package-source test counterparts, plus the always-required shell/security and native Hatch files. The unchanged quality matrix retains full-suite ownership. External trusted pytest config also requires an explicit subject root and xunit1 file attribution. Every selected file must appear; any required skip is incomplete. + +Native Hatch initially found a cached Python 3.11.14 when the clean controller PATH omitted the configured 3.11.15 directory. The job now exposes that exact selected directory while keeping trusted analyzer binaries first. It retains the exact-version rejection. The proof workspace is explicit and external. + +## Complete native engineering proof + +The independently installed signed reviewer 0.51.2 with core 0.55.4 completed all ten members. The final detached native profile passed 566 targeted cases in 36.61 seconds with zero failures, errors or skips. Project-runtime coverage was 87.77969018932875 percent against the repository's 80-percent requirement. Tracked source bytes and modes had equal before/after identities. The real existing/cold receipt uses project Python 3.11.15 and controller Python 3.12.13. + +The evidence is `/private/tmp/513-native-ci-probe-zY1ldc/output-passing/{receipt.json,tests.xml,coverage.json}` and `/private/tmp/513-native-ci-probe-zY1ldc/passing-hatch-proof/receipt.json`. This is local engineering evidence with observed local analyzer versions. Hosted Linux uses the existing reviewed exact analyzer version constraints and remains a separate blocking obligation. The temporary fixture commit is not delivery or protected authority. The final controller and workflow match the passing snapshot; the subsequent test-only helper extraction preserves every assertion and removes its complexity advisory. + +The retained customer-gate file moved to native CI ownership. All 63 cases pass. AST comparison proves all 44 other functions unchanged and only two prerequisite tests migrated. The formerly optional capsule flag cannot waive native review. The exact comparison is `/private/tmp/513-customer-test-migration-proof.json`. + +The real signed Semgrep bug adapter retained the eval/exec security finding with no ignore, candidate .gitignore, and candidate .semgrepignore. Its signed path reconciliation additionally rejects missing, duplicated or skipped inputs. No ignore shim, capsule repair or analyzer suppression was added. + +## Quality and independent review + +The local pre-commit pipeline passed formatting, YAML, imports, lint/types, Requirements evidence and 28 contracts. All seven strict filesystem module signatures passed with the established pinned public key. SMART and full-suite runs each passed 5,916 tests, with 77 existing opt-in skips, five baseline warnings and 95 passing subtests. Those broad runs collected the initial 22 new cases; the two later evidence-verifier cases are independently passing in the final 24-case run and the complete native profile. No unchanged broad gate is relabeled as a later full-tree run. + +CodeRabbit completed the full initial 17-file patch with one minor specification-drift finding, now corrected. Its final test-owner delta (four files) and native script delta (three files) each completed with zero findings. Independent source and 24-selector mapping review accepted the final controller and workflow, with the complete native receipt inspected. Required CodeRabbit export was explicitly approved for this prepared patch. Local review records do not grant protected pr_range/core #680/C15 authority. + +The first strict signature invocation omitted its public-key argument and failed before verification. That setup failure is retained; the corrected strict verification passed all seven manifests. The local Darwin dispatcher component remains explicitly DEFERRED until exact-head hosted signed native review passes. It is never reported as a local PASS. + +## Explicit advisory dispositions + +All raw findings remain in the native receipt. No tool error, unknown result, incomplete member, security finding or required execution is waived. + +- The old customer-gate test body's nesting/parameter/duplicate-shape and legacy pylint advisories are retained through a proven ownership move. The 44 unchanged function bodies are historical validation coverage, not new clean-code regressions. Rewriting capsule test behavior would violate the approved boundary. +- Pytest fixture-name shadowing is its injection protocol. Renaming the fixture to a private function triggered the required unused-function type check; the public fixture and its parameter names are retained. Test-local imports are scoped test setup and are not runtime module dependencies. +- Controller C0415 advisories preserve the verified-reviewer preload boundary. Reviewer imports must follow registry initialization in the trusted directory and precede candidate entry. Its R0914 inventory-local count is retained in the single explicit review owner rather than adding wrappers solely to satisfy a numeric style threshold. +- Scope-checker print-in-src advisories describe its CLI stdout diagnostics, which are asserted by callers. They do not identify application-layer printing; substituting another write API would only evade the rule. +- Missing-icontract advisories are retained for the existing three native service boundaries (`inspect_project`, `PreparedProject.run`, `prepare_project`), the existing standard-library scope/proof entrypoints/helpers, and the new CI CLI entrypoint. This CI transition does not add runtime preconditions or admission rules solely to satisfy a universal decorator heuristic. These exact advisories are documented exceptions, not absent contract execution; the signed contract/CrossHair member completed, and typed failure behavior plus actual native execution remain required. + +## Delivery + +Signed delivery and exact-head hosted outcomes are recorded after push. Merge remains closed until the required native review, native Hatch, scope, signatures, quality matrix and compatibility checks pass on that exact head. Do not archive this change until actual completion and merge through OpenSpec CLI. + +## Exact-head Linux bootstrap correction + +Head `415abf69a1c98053c8ecdb6f88d4f7bc7dc617b6`, run 40230445251, failed before analyzer execution. Pip correctly rejected signed core 0.55.4 requiring Click >=8.3.3 alongside pinned Semgrep 1.144.0 requiring Click 8.1.x. No analysis result was accepted or retried. + +The native workflow now installs the same trusted-base Semgrep pin in its own CLI environment. Controller dependencies retain trusted-base constraints; Semgrep remains on the sanitized controller PATH and its actual executable version enters the receipt. This changes CI packaging only, with no candidate runtime dependency, signed analyzer, finding-policy or admission change. + +Two focused regressions failed before implementation. The generated controller requirements still contained Semgrep, reproducing the conflicting ownership, and executable version collection was absent. After the fix all 26 transition cases passed with zero skips. Actual hosted dependency resolution and all ten review members remain blocking on the next signed event head. + +## Retained Semgrep transitive constraints + +Head `b223f55590f5f8c51e2b0b9575b1f19aa4f197ac`, run 40230829939, installed both environments successfully but failed at `semgrep --version`. A disposable native environment with Semgrep 1.144.0 reproduced the startup ImportError: MCP 1.16.0 imported `eval_type_backport`, removed from the newly resolved Pydantic 2.14.1. This disproved the assumption that the retained direct analyzer pin alone reproduced its runtime. + +The same native CLI started successfully after applying the existing trusted-base Linux wheel versions as transitive constraints, including Pydantic 2.13.4. CI now retains those recorded versions for the isolated Semgrep environment. No capsule tooling or lock file changed. A strengthened requirements-generation test failed because the constraints file was absent; a new startup-error test failed because CalledProcessError hid stderr. All 27 focused cases now pass. The failure remains fail-closed and retains bounded stderr for future diagnosis. Hosted exact-head acceptance remains pending. + +## Paired-core runtime smoke handoff + +Head `955fe48e2b8a92fecfc204633573af635008dfcc` passed independent Linux native review in run 40231532223. The verified artifact has all ten COMPLETE members, 569 passing targeted cases with zero skips/errors/failures, unchanged tracked source and 87.78% project-runtime coverage. All configured analyzer versions match their retained pins. + +The newly released quality 3.12 job 121284917529 then reached a pre-existing handoff error. The paired core requires its authenticated portable fixture at commit `046d366c190af7a188d6f48953a96edb65e766db`, tree `c5b44db3477d1c9ea8dae1dceab2af276d265353`; the modules workflow supplied the PR checkout. Core correctly rejected that identity before module archive execution. Current modules dev remains the branch ancestor, so no blind retry or rebase is justified. + +CI now follows the existing paired-core setup action's fixture contract, checking the reviewed lock, exact commit/tree and clean checkout before running the same six launchers. Pipx/uv/virtualenv launcher tooling gets a separate environment, avoiding the observed virtualenv<21 collision with installed Hatch requiring virtualenv>=21. The candidate still owns all three full quality cells, native review, native Hatch acceptance, signatures and minimum-core compatibility. Fixture smoke proves package-manager integration; it does not claim candidate-module acceptance. No paired-core source, lock, capsule, runtime or admission rule changes. + +The three handoff cases failed before implementation because fixture setup was absent; the actual Linux rejection supplies the behavior-level RED. After adaptation all 30 transition cases and twelve existing orchestrator/signing cases pass. Hosted verification of the revised handoff remains blocking. + +## Follow-up native test discovery and smoke seeding + +PR review comment 4372310796 reproduced mandatory rejection of existing nonmirrored package tests. The focused regression failed before implementation; a missing-package control already passed. Selection now prefers an existing mirrored file, otherwise enumerates the existing package unit tests as explicit file arguments. A package without unit tests still fails, and the existing zero-skip/per-file evidence checks remain unchanged. This adds no customer test-layout or runtime admission requirement. + +Head `c7b9ce6d` passed independent native review, then quality 3.12 job 121287402697 reached the authenticated fixture but correctly rejected offline synchronization from an unseeded core cache. The workflow now follows paired core's existing frozen setup contract: validate dependency trust, synchronize its locked all-extras graph into a separate seed environment, then retain its offline child sync and all six launchers. Launcher executables come from that frozen graph rather than independently resolving pipx/virtualenv against Hatch. No core source or lock is changed. Two focused setup regressions failed before adaptation. Exact-head hosted validation remains mandatory. + +## Coverage-root test inventory correction + +Review comment 4372422865 reproduced empty selection for tools/contract_first_smart_test.py and top-level src/specfact_cli_modules/dev_bootstrap.py. Both focused cases failed with an empty inventory before the fix. Native selection and coverage now share the same trusted coverage-root tuple. Existing mirrored paths remain preferred, flat exact/variant test filenames are recognized, and package unit discovery remains the final fallback. Sources with no applicable tests still fail closed. All 35 CI cases pass after the correction; module payload/signature bytes are unchanged. The core trust remediation remains explicitly deferred because it overlaps the active core #785 recovery. No core changes or capsule repairs were made. + +## Changed-source type projection + +Native review at signed heads 670d403a and content-identical 20819d9b stopped as INCOMPLETE when pinned BasedPyright exceeded its unchanged 30-second timeout. The single fresh-build hypothesis did not hold, so no further blind retry was issued. Inspection found that the CI projection scanned every repository directory while the signed adapter retained diagnostics only for the exact changed-file inventory. The new projection regression failed before the correction with five directory includes instead of the two expected changed-file includes. The wrapper now passes that existing authenticated inventory into configuration projection. Trusted type settings, execution environments and import paths remain intact. A real BasedPyright 1.39.10 regression detects a changed file's incompatible imported return type. All 37 focused cases pass. The signed analyzer, timeout, raw completeness checks and full-repository quality type gate are unchanged. Exact-head hosted acceptance remains required; this local result does not waive either paired-core blocker. + +## Paired-core quality contract, 2026-11-03 + +The unchanged PR head 5baeacc5 has one failed workflow. Quality 3.11 job 121298724859 and 3.13 job 121298724910 each report 5,915 passing tests and one failure: declared immutable core ==0.55.4 was compared with intentionally installed paired core 0.55.11. The existing bootstrap explicitly selects and restores that editable checkout. Quality 3.12 job 121298724856 independently rejects the expired pycparser exception. Current public core dev still records expiry 2026-10-22, and its active recovery session retains ownership. No core, trust register, lock or capsule repair is included here. + +Read-only review confirmed that released and paired installations have separate existing owners. The bounded correction always checks the exact ==0.55.4 declaration. Unpaired execution still requires installed0.55.4 and both registry APIs. Explicit paired execution additionally proves editable distribution URL, matching checkout version and actual root/registry import origins. An unconditional isolated native-workflow step verifies released0.55.4 and both APIs before candidate bootstrap, regardless of changed-file selection. The complete paired-core quality matrix remains intact. + +Before implementation, focused regressions produced five failures and one passing control. The valid paired version hit the known ==0.55.4 versus ==0.55.11 mismatch; noneditable, different-checkout and registry-origin controls reported DID NOT RAISE; the unconditional released-core step was absent. A later root-package provenance control separately failed with DID NOT RAISE before that guard. Local logs are /private/tmp/524-20261103/red.txt and core-origin-red.txt. Passing evidence and independent final review are recorded with delivery; local results cannot waive the separate core trust failure or claim merge readiness. + +## Explicit nonmirrored tool correspondence, 2026-11-03 + +New review comment 4460364477 reproduced selection of tests/unit/tools/test_contract_first_smart_test.py for tools/smart_test_coverage.py while omitting its two existing direct test files. The focused regression failed before implementation with the unrelated file as an extra item and both applicable files missing; two missing-test controls already passed. An explicit correspondence now selects tests/unit/test_capsule_proof_contexts.py and tests/unit/specfact_code_review/run/test_reviewer_bootstrap.py before filename/directory fallback. Either missing mapped file fails closed. Existing test contents, production tool source and per-file coverage policy remain unchanged. This corrects selection without certifying otherwise unmeasured legacy-tool coverage. Failing-first evidence is /private/tmp/524-20261103/inventory-red.txt. Passing evidence and final exact-head verification are recorded with delivery. diff --git a/openspec/changes/code-review-native-pr-ci-transition/design.md b/openspec/changes/code-review-native-pr-ci-transition/design.md new file mode 100644 index 00000000..d0a506af --- /dev/null +++ b/openspec/changes/code-review-native-pr-ci-transition/design.md @@ -0,0 +1,11 @@ +# Native CI review owner + +The workflow installs the signed released reviewer separately from candidate dependencies. An isolated controller imports that installation before entering the subject. Candidate reviewer modules cannot replace the controller. Project dependencies remain in the repository Hatch environment. Independent analyzer tools use a separate controller environment constrained by the existing reviewed analyzer versions. Signing secrets are absent and checkout credentials are not persisted. + +The input is the exact event head, merge-base, staged head tree, trusted reviewer identity and rule/configuration identities. The output is a CI receipt with one completed outcome per required member, raw findings and the ordinary changed-enforcement result. It is ordinary job evidence, not a protected report or a new customer report schema. + +Reuse the signed tool adapters for Ruff, Radon, Semgrep clean and bugs, AI-bloat AST, AST clean code, basedpyright, pylint and contracts/CrossHair. Require the signed rule packs and explicit trusted configuration. Capture each member before filtering. Tool errors, UNKNOWN, skipped or malformed execution block regardless of line or severity. Run native project tests with coverage and retain their actual result. Test applicability must be explicit rather than inferred from an empty result. + +Validate source identity before and after execution. Bind the receipt and artifact to the event head. The quality matrix requires successful independent review, native Hatch acceptance, migration scope and signatures. A skipped, cancelled, stale or incomplete prerequisite cannot pass. + +One writer implements this bounded adapter. Source analysis compared a static supplement with full signed-engine reuse. Full reuse preserves existing ownership with less duplicated policy. The critical probe demonstrated engine callability and exposed its missing completeness barrier. Independent implementation review and exact-head Linux execution remain required before removing the old prerequisite. diff --git a/openspec/changes/code-review-native-pr-ci-transition/proposal.md b/openspec/changes/code-review-native-pr-ci-transition/proposal.md new file mode 100644 index 00000000..e8330f91 --- /dev/null +++ b/openspec/changes/code-review-native-pr-ci-transition/proposal.md @@ -0,0 +1,23 @@ +# Native PR CI transition + +## Why + +The maintainer explicitly authorized advancing the CI/security transition before merging PR #524 on 2026-10-25, Europe/Berlin. Capsule repairs remain excluded. Earlier instructions to defer this transition are superseded only for repository CI. The customer runtime sequence 17 → 18 → 19 → paired core → 20 remains separate. + +## What changes + +Replace the required capsule customer prerequisite with exact-head native acceptance and an independently installed signed reviewer. Preserve the complete ten-member review inventory, contracts, test coverage, changed enforcement, module signatures and the existing quality matrix. Missing tools, errors, unknown evidence, missing rule packs and stale source remain nonaccepting. Run the existing shell/security and native Hatch cases in mandatory native CI. Move the retained customer-gate test file to native CI ownership, preserving all 44 other functions and migrating only its two orchestrator prerequisite tests. + +The CI adapter reuses signed reviewer 0.51.2 and core 0.55.4. It does not change the public review dispatcher, introduce a backend selector, publish a release, or produce protected pr_range, core #680 or C15 assurance. Native execution retains the approved non-adversarial project assumption. Dependency installation, hooks, imports and tests execute in a credential-free disposable runner. No hostile-code containment is claimed. + +## Capabilities + +### New capabilities +- `native-pr-ci`: complete, independent, exact-head native repository review. + +### Modified capabilities +None. + +## Alternatives and rollback + +A prerequisite deletion or static-only scan drops existing obligations. Bare run_review returned exit zero with a CrossHair UNKNOWN in a real probe and cannot own acceptance. Reuse the existing signed adapters behind one CI completeness barrier. Rollback restores the previous prerequisite through a reviewed revert and restores its blocking outcome. diff --git a/openspec/changes/code-review-native-pr-ci-transition/requirements-evidence.yaml b/openspec/changes/code-review-native-pr-ci-transition/requirements-evidence.yaml new file mode 100644 index 00000000..8a8112d7 --- /dev/null +++ b/openspec/changes/code-review-native-pr-ci-transition/requirements-evidence.yaml @@ -0,0 +1,574 @@ +{ + "schema_version": "2", + "requirements": { + "openspec:code-review-native-pr-ci-transition:native-pr-ci:preserve-complete-independent-native-review": { + "rationale": "Explicit maintainer authorization advances only CI/security before PR524 merge.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "preserve-complete-independent-native-review", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-pr-ci-transition/specs/native-pr-ci/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "preserve-complete-independent-native-review-9", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_raw_incomplete_findings_block_even_when_policy_exit_is_zero[finding0]" + } + }, + { + "case_id": "preserve-complete-independent-native-review-10", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_raw_incomplete_findings_block_even_when_policy_exit_is_zero[finding1]" + } + }, + { + "case_id": "preserve-complete-independent-native-review-11", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_raw_incomplete_findings_block_even_when_policy_exit_is_zero[finding2]" + } + }, + { + "case_id": "preserve-complete-independent-native-review-12", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_required_signed_rule_pack_cannot_silently_disappear" + } + }, + { + "case_id": "preserve-complete-independent-native-review-15", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_all_signed_semgrep_passes_are_required" + } + }, + { + "case_id": "preserve-complete-independent-native-review-16", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_native_tools_are_installed_from_base_before_project_bootstrap" + } + }, + { + "case_id": "preserve-complete-independent-native-review-17", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test__native_test_command_uses_trusted_config" + } + }, + { + "case_id": "preserve-complete-independent-native-review-19", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_project_tests_do_not_inherit_controller_diff_mode" + } + }, + { + "case_id": "preserve-complete-independent-native-review-21", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_no_python_sources_requires_positive_applicability" + } + }, + { + "case_id": "preserve-complete-independent-native-review-22", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_boundary_script_imports_through_native_contract_analyzer" + } + }, + { + "case_id": "preserve-complete-independent-native-review-23", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Reject a skipped required source test in actual JUnit evidence.", + "observable": "A targeted source test skip makes native execution incomplete.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_targeted_source_test_skip_is_incomplete" + } + }, + { + "case_id": "preserve-complete-independent-native-review-24", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Reject a selected test file with no executed JUnit cases.", + "observable": "Every explicit selected file must appear in pytest source attribution.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_selected_test_file_cannot_disappear_from_evidence" + } + }, + { + "case_id": "semgrep-isolation", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep incompatible analyzer CLI dependencies separate from the signed controller.", + "observable": "Pinned Semgrep remains selected and its invoked version is recorded without imposing its Click dependency on core.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_semgrep_dependencies_are_separate_from_signed_controller" + } + }, + { + "case_id": "semgrep-version", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep incompatible analyzer CLI dependencies separate from the signed controller.", + "observable": "Pinned Semgrep remains selected and its invoked version is recorded without imposing its Click dependency on core.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_semgrep_version_comes_from_selected_executable" + } + }, + { + "case_id": "semgrep-startup-diagnostics", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep native analyzer startup errors actionable without accepting incomplete evidence.", + "observable": "A failed Semgrep startup retains bounded stderr in the incomplete receipt exception.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_semgrep_startup_failure_preserves_bounded_stderr" + } + }, + { + "case_id": "changed-type-inventory", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Analyze every changed Python file with trusted type settings and import context.", + "observable": "Projection matches the complete changed-source inventory and real pinned BasedPyright still reports imported type errors without scanning unrelated files.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_type_projection_covers_exact_changed_python_inventory" + } + }, + { + "case_id": "imported-type-error", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Analyze every changed Python file with trusted type settings and import context.", + "observable": "Projection matches the complete changed-source inventory and real pinned BasedPyright still reports imported type errors without scanning unrelated files.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_type_projection_retains_imported_type_errors" + } + }, + { + "case_id": "immutable-core-api-owner", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_immutable_core_registry_apis_are_checked_before_candidate_bootstrap" + } + }, + { + "case_id": "real-core-registry-handoff", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_declared_core_dependency_supplies_registry_handoff" + } + }, + { + "case_id": "explicit-paired-core", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_explicit_paired_core_keeps_registry_handoff" + } + }, + { + "case_id": "reject-paired-noneditable", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_paired_core_registry_handoff_rejects_unrelated_installation[noneditable]" + } + }, + { + "case_id": "reject-paired-other-checkout", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_paired_core_registry_handoff_rejects_unrelated_installation[other-checkout]" + } + }, + { + "case_id": "reject-paired-core-origin", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_paired_core_registry_handoff_rejects_unrelated_installation[core-origin]" + } + }, + { + "case_id": "reject-paired-registry-origin", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_paired_core_registry_handoff_rejects_unrelated_installation[registry-origin]" + } + }, + { + "case_id": "reject-paired-version", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_paired_core_registry_handoff_rejects_unrelated_installation[version]" + } + }, + { + "case_id": "reject-paired-unpaired", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Keep immutable release verification separate from intentionally selected paired-core quality execution.", + "observable": "Exact0.55.4 declaration and unpaired release remain required; paired mode verifies editable metadata and actual root/registry origins; unconditional independent release API checks precede bootstrap.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_paired_core_registry_handoff_rejects_unrelated_installation[unpaired]" + } + }, + { + "case_id": "nonmirrored-tool-tests", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Select the known existing tests for a nonmirrored tool before directory fallback.", + "observable": "Both explicit smart-test files are selected and either missing mapped file blocks acceptance; coverage remains independently required.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_nonmirrored_tool_selects_its_existing_test_files" + } + }, + { + "case_id": "missing-context-map", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Select the known existing tests for a nonmirrored tool before directory fallback.", + "observable": "Both explicit smart-test files are selected and either missing mapped file blocks acceptance; coverage remains independently required.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_nonmirrored_tool_rejects_missing_mapped_test[context]" + } + }, + { + "case_id": "missing-bootstrap-map", + "scenario_id": "preserve-complete-independent-native-review", + "method": "test", + "intent": "Select the known existing tests for a nonmirrored tool before directory fallback.", + "observable": "Both explicit smart-test files are selected and either missing mapped file blocks acceptance; coverage remains independently required.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_nonmirrored_tool_rejects_missing_mapped_test[bootstrap]" + } + } + ] + }, + "openspec:code-review-native-pr-ci-transition:native-pr-ci:bind-acceptance-to-exact-source-and-required-jobs": { + "rationale": "Explicit maintainer authorization advances only CI/security before PR524 merge.", + "stakeholder_refs": [ + "nold-ai/specfact-cli-modules#513" + ], + "touchpoints": [ + { + "id": "bind-acceptance-to-exact-source-and-required-jobs", + "kind": "config_file", + "locator": "openspec/changes/code-review-native-pr-ci-transition/specs/native-pr-ci/spec.md" + } + ], + "verification_cases": [ + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-1", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_complete_native_review_accepts_exact_head" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-2", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_incomplete_native_review_cannot_pass[missing]" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-3", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_incomplete_native_review_cannot_pass[duplicate]" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-4", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_incomplete_native_review_cannot_pass[unknown]" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-5", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_incomplete_native_review_cannot_pass[stale]" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-6", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_incomplete_native_review_cannot_pass[source]" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-7", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_incomplete_native_review_cannot_pass[policy]" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-8", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_incomplete_native_review_cannot_pass[malformed]" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-13", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_quality_requires_native_review_without_capsule_prerequisite" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-14", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_independent_native_job_uses_signed_reviewer_and_exact_subject" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-18", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_darwin_deferral_requires_indexed_native_replacement" + } + }, + { + "case_id": "bind-acceptance-to-exact-source-and-required-jobs-20", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Execute the actual native CI boundary and reject incomplete evidence.", + "observable": "No stale source, unavailable analyzer, skipped mandatory case or missing independent prerequisite can grant acceptance.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_subject_detects_assume_unchanged_mutation" + } + }, + { + "case_id": "runtime-fixture-accepted", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Preserve the paired core reviewed fixture contract without bypassing candidate quality.", + "observable": "Unreviewed fixture identities are rejected and all six launcher checks remain mandatory with separate launcher dependencies.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_runtime_smoke_rejects_unreviewed_fixture_identity[046d366c190af7a188d6f48953a96edb65e766db]" + } + }, + { + "case_id": "runtime-fixture-rejected", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Preserve the paired core reviewed fixture contract without bypassing candidate quality.", + "observable": "Unreviewed fixture identities are rejected and all six launcher checks remain mandatory with separate launcher dependencies.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_runtime_smoke_rejects_unreviewed_fixture_identity[0000000000000000000000000000000000000000]" + } + }, + { + "case_id": "runtime-launcher-ownership", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Preserve the paired core reviewed fixture contract without bypassing candidate quality.", + "observable": "Unreviewed fixture identities are rejected and all six launcher checks remain mandatory with separate launcher dependencies.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_runtime_smoke_preserves_launchers_and_isolates_dependencies" + } + }, + { + "case_id": "existing-package-test-layout", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Preserve existing test layouts and complete paired-core smoke prerequisites.", + "observable": "Existing package tests are selected, absent package tests fail closed, and trusted frozen cache seeding precedes offline execution.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_existing_nonmirrored_package_tests_are_selected" + } + }, + { + "case_id": "missing-package-tests-rejected", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Preserve existing test layouts and complete paired-core smoke prerequisites.", + "observable": "Existing package tests are selected, absent package tests fail closed, and trusted frozen cache seeding precedes offline execution.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_missing_package_tests_cannot_pass_selection" + } + }, + { + "case_id": "frozen-smoke-cache-seeding", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Preserve existing test layouts and complete paired-core smoke prerequisites.", + "observable": "Existing package tests are selected, absent package tests fail closed, and trusted frozen cache seeding precedes offline execution.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_runtime_smoke_seeds_frozen_cache_before_offline_execution" + } + }, + { + "case_id": "coverage-root-tool", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Select applicable existing tests for every coverage-enforced production root.", + "observable": "The reported tool module and flat-layout top-level source tests are selected before per-file coverage enforcement.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_coverage_roots_select_existing_source_tests[tool]" + } + }, + { + "case_id": "coverage-root-top-level-src", + "scenario_id": "bind-acceptance-to-exact-source-and-required-jobs", + "method": "test", + "intent": "Select applicable existing tests for every coverage-enforced production root.", + "observable": "The reported tool module and flat-layout top-level source tests are selected before per-file coverage enforcement.", + "selector": { + "runner": "pytest", + "node_id": "tests/unit/scripts/test_native_pr_review.py::test_coverage_roots_select_existing_source_tests[top-level-src]" + } + } + ] + } + } +} diff --git a/openspec/changes/code-review-native-pr-ci-transition/specs/native-pr-ci/spec.md b/openspec/changes/code-review-native-pr-ci-transition/specs/native-pr-ci/spec.md new file mode 100644 index 00000000..4459d500 --- /dev/null +++ b/openspec/changes/code-review-native-pr-ci-transition/specs/native-pr-ci/spec.md @@ -0,0 +1,61 @@ +## ADDED Requirements + +### Requirement: Preserve complete independent native review +The repository SHALL require a separately installed verified signed reviewer, all ten existing review members, native test coverage and module signatures before accepting a code pull request. It SHALL preserve changed enforcement and independent security without executing capsule review. + +#### Scenario: Complete native review +- **WHEN** the exact candidate has complete native analyzer and test evidence +- **THEN** the required native job may pass under the existing finding policy + +#### Scenario: Incomplete analyzer cannot pass +- **WHEN** a required member is absent, duplicated, skipped, unknown, malformed, timed out or has a tool error +- **THEN** acceptance fails even if the ordinary engine exit is zero + +#### Scenario: Trusted reviewer and configuration +- **WHEN** candidate code attempts to shadow reviewer imports or omit required rules +- **THEN** the controller retains the signed installation and rejects missing control inputs + +#### Scenario: Changed-source type analysis +- **WHEN** independent review checks the exact changed Python inventory +- **THEN** its type-analysis projection includes every changed Python file, retains trusted type settings and import context, and does not scan unrelated files whose diagnostics the signed adapter discards +- **AND** timeouts remain incomplete and the full-repository quality type gate remains required + +#### Scenario: Conflicting analyzer dependencies +- **WHEN** a pinned analyzer CLI has dependencies incompatible with the signed controller +- **THEN** CI installs that CLI separately, preserves its trusted pin and recorded transitive constraints, records the version of the executable actually invoked and retains bounded startup failure diagnostics + +### Requirement: Bind acceptance to exact source and required jobs +The native CI receipt SHALL bind the event head and complete required-member inventory. The quality matrix SHALL reject every non-successful native prerequisite. Native receipts SHALL NOT satisfy protected pr_range, core #680 or C15 contracts. + +#### Scenario: Reject stale evidence +- **WHEN** a receipt belongs to another head or source changes during execution +- **THEN** acceptance fails + +#### Scenario: Preserve mandatory prerequisites +- **WHEN** signatures, native Hatch, independent review or migration scope fail or are skipped +- **THEN** quality cannot pass + +#### Scenario: Existing package test layouts +- **WHEN** a changed package source lacks a mirrored unit-test filename +- **THEN** native review selects the package's existing unit-test files without requiring a new layout, and rejects a package with no unit tests + +#### Scenario: Test selection follows coverage roots +- **WHEN** a changed production source belongs to a configured coverage root, including `tools/` or top-level `src/` +- **THEN** native review selects its existing mirrored, flat or package unit tests before enforcing coverage, using the same trusted root inventory for both operations + +#### Scenario: Explicit nonmirrored test correspondence +- **WHEN** a production source has known tests outside its mirrored or directory layout +- **THEN** native review selects all explicitly mapped existing test files before directory fallback, and rejects a missing mapped file +- **AND** the applicable per-file coverage policy remains required after selection + +#### Scenario: Preserve paired-core smoke ownership +- **WHEN** the paired core requires its reviewed portable fixture for package-manager smoke +- **THEN** CI authenticates that fixture and runs all six existing launchers with isolated launcher dependencies, while candidate source remains subject to the complete quality, native acceptance and signing gates +- **AND** fixture smoke does not claim candidate module acceptance +- **AND** CI authenticates and seeds the paired core's frozen dependency cache before its offline child synchronization + +#### Scenario: Preserve released and paired core verification +- **WHEN** quality intentionally installs the selected paired core checkout instead of the immutable development release +- **THEN** the complete quality matrix verifies its editable distribution identity and registry API import origins against that selected checkout +- **AND** the declared development core remains exactly version 0.55.4, and the independent native job always verifies that released version and both registry APIs before candidate bootstrap +- **AND** an unrelated or unproven installation cannot satisfy either verification mode diff --git a/openspec/changes/code-review-native-pr-ci-transition/tasks.md b/openspec/changes/code-review-native-pr-ci-transition/tasks.md new file mode 100644 index 00000000..ed820f88 --- /dev/null +++ b/openspec/changes/code-review-native-pr-ci-transition/tasks.md @@ -0,0 +1,10 @@ +# Tasks + +- [x] Record explicit maintainer authorization and bounded alternatives. +- [x] Probe the existing engine and identify the completeness gap. +- [x] Write focused negative tests and record failing-before evidence. +- [x] Implement CI-only signed-engine reuse and native prerequisite transition. +- [x] Prove native execution and all fail-closed cases. +- [x] Run required gates and independent implementation review. +- [ ] Deliver signed commits and verify exact-head Linux results. +- [ ] Archive through OpenSpec CLI after completion and merge. diff --git a/openspec/native-review-policy.json b/openspec/native-review-policy.json new file mode 100644 index 00000000..c9025b3a --- /dev/null +++ b/openspec/native-review-policy.json @@ -0,0 +1,187 @@ +{ + "execution_backend": "trusted-local-native", + "freeze_ref": "23967ee8dc22545935427cd737b418e5b21198e5", + "legacy_paths": [ + ".github/workflows/*capsule*.yml", + ".github/workflows/code-review-macos-boundary.yml", + "packages/specfact-code-review/src/specfact_code_review/run/*", + "scripts/*capsule*.py", + "scripts/*macos*.py", + "scripts/native_release/*", + "scripts/native_analyzer_inputs/*", + "scripts/macos_managed_boundary/*", + "tests/native/*", + "tests/fixtures/portable-runtime/*", + "tests/unit/*capsule*.py", + "tests/host/proof_capsule*" + ], + "baseline_files": { + ".github/workflows/capsule-customer-execution.yml": ["100644", "53a41c25b5b5701b0dd14725dc4b33a43437056a"], + ".github/workflows/code-review-macos-boundary.yml": ["100644", "50371e128983f91b76282f749f7ccf8bad4759e9"], + ".github/workflows/native-capsule-customer.yml": ["100644", "8fd4eb6d55dac168d6358eabd96556429d1fc05e"], + ".github/workflows/native-capsule-release.yml": ["100644", "bba5ca7f3ac607e6892706f1b16b5a3f72aa51f5"], + "packages/specfact-code-review/src/specfact_code_review/run/__init__.py": ["100644", "4d54cc5eefef51d3ccfc158659685ff28d1f8039"], + "packages/specfact-code-review/src/specfact_code_review/run/cleanup_evidence.py": ["100644", "26864048d2341d06053be54986b47b82bc038d3b"], + "packages/specfact-code-review/src/specfact_code_review/run/commands.py": ["100644", "8fb1c4d736283d55f043f9afe496553f738ebe4d"], + "packages/specfact-code-review/src/specfact_code_review/run/differential.py": ["100644", "ae13fb0f40a91e3840f190dea9f7cd0bc6e50c58"], + "packages/specfact-code-review/src/specfact_code_review/run/findings.py": ["100644", "e7e55e64ff162c97aefd2612457a8964f026ddd6"], + "packages/specfact-code-review/src/specfact_code_review/run/forecast.py": ["100644", "fe239b06dd9a3a7aa919714294816d4d3bfe5a16"], + "packages/specfact-code-review/src/specfact_code_review/run/installed_coverage.py": ["100644", "2817d2f73f59c51897d834d6c0a8dff5876a9ec8"], + "packages/specfact-code-review/src/specfact_code_review/run/native_analyzer_view.py": ["100644", "ed4f19f075c53fe85abaf74cb795168f0bccfb9e"], + "packages/specfact-code-review/src/specfact_code_review/run/native_backend.py": ["100644", "b5c35a6ca85d30578e294ccac540b5dc1e469f72"], + "packages/specfact-code-review/src/specfact_code_review/run/native_capsule.py": ["100644", "104a005c6435b5fd7f9b45d70a587ab7768a89a0"], + "packages/specfact-code-review/src/specfact_code_review/run/native_child_worker.py": ["100644", "fcd676b551899d0376e41de32fbe6d199b449045"], + "packages/specfact-code-review/src/specfact_code_review/run/native_execution.py": ["100644", "0eec3c013adea35c53612633d08a37fea9ab43d4"], + "packages/specfact-code-review/src/specfact_code_review/run/native_managed_process.py": ["100644", "aa9a16a9a9184cb87f9cf5baa7770cb8ed5a0e1e"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_catalog.py": ["100644", "c9fdee84a42fd6024a8125010169f25725363625"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_hatch.py": ["100644", "3eebe00af7e196fe89ae8ec28daa7dc793e1954c"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_hooks.py": ["100644", "e61cd7ba33390947311f8f6cec40b8c088c1278a"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_manager.py": ["100644", "d6d3eb0603da99216dd1d8f92c508db8364a393e"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_pip.py": ["100644", "a2687b248c3f724263f6e71da81320ae2a40883b"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_poetry.py": ["100644", "2713dcc41845b305925df0f729093ba4fe89bc0e"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_runtime.py": ["100644", "f1b7baad02ab6801671c102dd822092aa5561ba9"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_source.py": ["100644", "5bc2138c06621d7c4901fef0ab3bfd8884dfc099"], + "packages/specfact-code-review/src/specfact_code_review/run/native_project_uv.py": ["100644", "6bf021a9c95cbffb1017d572aa0c60bc20469071"], + "packages/specfact-code-review/src/specfact_code_review/run/native_pytest_plugins.py": ["100644", "e05aa4cf2be31556b30487fa1dcbcf7b15e86548"], + "packages/specfact-code-review/src/specfact_code_review/run/native_python_environment.py": ["100644", "dd9db37d4b3b93e8ab30da5a7c3c22312340fea0"], + "packages/specfact-code-review/src/specfact_code_review/run/native_tool_worker.py": ["100644", "8e79553ee3ca6a1ec9d783f184563317f38303d0"], + "packages/specfact-code-review/src/specfact_code_review/run/native_worker.py": ["100644", "ef04a397efd635227221b197fba1a6a0653ead6f"], + "packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py": ["100644", "4685c496782d6eab3ea264fd7a2164ead00a5ac4"], + "packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py": ["100644", "1d92f6103d23de0a969d3838330c1739db7a1dcb"], + "packages/specfact-code-review/src/specfact_code_review/run/runner.py": ["100644", "f6922071734583d5eea489cec3c30066dcc373dd"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_adapters.py": ["100644", "39366dbe056232be496a9368e15fd0310062cf28"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py": ["100644", "9207b5c38ab03419df8c3805ddfca0169a7aa078"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py": ["100644", "22d3a83e98fc3bbcf35aecdd0a5a4de699a29c9d"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py": ["100644", "4c7dda1b7ef8ff2449c52018d3d386c84b54965b"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_commands.py": ["100644", "3d30d39071f44b4a43006aee7256ee9531f43ff2"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_compatibility.py": ["100644", "0e17872f3a675c1a9258870632b4bfd66aec9484"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py": ["100644", "b537e361c64305f0a2b2362c6210c56770ee1f8b"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_domains.py": ["100644", "c885fb03de61aceece3d4431d34ff8fccaac5a57"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py": ["100644", "831c87c47a8fa59f6584a2a292d855c436842a61"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_interpreter.py": ["100644", "ff8ddf43589810d854885cbdf580ce459b4a4130"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_models.py": ["100644", "8997021b2e1327c601c0b33cba68e1965b59621e"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py": ["100644", "77efaec104c59f208e183b9fae9a036fa047f748"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py": ["100644", "cdc973db1f891a19edf44533c7ebd946e7ff1717"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_tools.py": ["100644", "db358327f52b80832715375859c66ed46d3c0e6d"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_trust.py": ["100644", "4a99972887dd2ceb157f809e642511773c4d1777"], + "packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py": ["100644", "e4ddb04572bf7cb996425a6f21cd1bd36afd39f0"], + "packages/specfact-code-review/src/specfact_code_review/run/sandbox.py": ["100644", "f0faf0686324b39a0b845d419521a6435857cbf7"], + "packages/specfact-code-review/src/specfact_code_review/run/scope.py": ["100644", "53460a8a9f6d6d4bced9fffcc32321e15a6bbf2c"], + "packages/specfact-code-review/src/specfact_code_review/run/scorer.py": ["100644", "d3c256d864a4d5c0a6efb37eb9872bb06ed82da1"], + "packages/specfact-code-review/src/specfact_code_review/run/sitecustomize.py": ["100644", "e515fe093ced8f16aff5b3877b806b1baa455183"], + "packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py": ["100644", "287160328a49c906e16f6be866b6d94c6dbb076a"], + "packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py": ["100644", "42c4424d6e13dc41564b725bd02c9fd8c8547271"], + "packages/specfact-code-review/src/specfact_code_review/run/target_crosshair.py": ["100644", "d38f921c4e629c0beaab5d05f6d26ee7f3751c5c"], + "packages/specfact-code-review/src/specfact_code_review/run/target_launch.py": ["100644", "3c4d612e4ce2863b10b4477b0c741593eef5e0ec"], + "packages/specfact-code-review/src/specfact_code_review/run/target_pylint.py": ["100644", "b4adb92aa1e2a4e7a80b2221be3774f3e0547b82"], + "packages/specfact-code-review/src/specfact_code_review/run/target_pytest.py": ["100644", "a8d796a65492d633ff9b138de1ef587540a7ff8d"], + "packages/specfact-code-review/src/specfact_code_review/run/toolchain.py": ["100644", "d4dbf0e065c93def7e46959f09c71eb3b7200a13"], + "scripts/assemble_macos_native_capsule.py": ["100644", "248ba9d2b64044ba6aff9bbb90ddfc7867fa8faf"], + "scripts/build_macos_managed_git.py": ["100644", "25b17df76eaa59872d1dd6387a285b4f5cc7bb1a"], + "scripts/build_macos_managed_uv.py": ["100644", "e36771c1a82bba439c8177e4cc9f5bbb42492a2b"], + "scripts/build_macos_native_capsule.py": ["100644", "e08faa058a3d948fb89fa7b80182f999e6b16d63"], + "scripts/capsule_customer_gate.py": ["100644", "a0a97fc550beaef2f62c0bacf5dbb629dde57faf"], + "scripts/capsule_profile_summary.py": ["100644", "6ba7081f51a1a9e11467a98c218784e7a7271377"], + "scripts/check_macos_boundary_ci_receipts.py": ["100644", "6b8e65d129e18eff27e2c69697229adb33f0bce6"], + "scripts/external_capsule_corpus.py": ["100644", "74bee19925fd6ef756e613b1934c911c1973f753"], + "scripts/macos_capsule_candidate.py": ["100644", "da321d6b44a33622c4b960b698e6f4bbdd139ef6"], + "scripts/macos_execution_boundary/build.py": ["100644", "2865af1bad57b3020c350bfe4e2761d9cecb0038"], + "scripts/macos_execution_boundary/run.py": ["100644", "4832df0c8dcbb72744d7780f898243dbc29ef763"], + "scripts/macos_managed_boundary/README.md": ["100644", "46d7dc25559a7e3e027b9fcee1b75e261eb3e332"], + "scripts/macos_managed_boundary/analyzer.py": ["100644", "26f65b449107d54e928dfce0d1b72b4a666cf602"], + "scripts/macos_managed_boundary/analyzer.sb": ["100644", "6e9857fa089e4c0adb0bfbd342cbc50abff22176"], + "scripts/macos_managed_boundary/analyzer_worker.c": ["100644", "df7ae74b58bb1701d3996bbfffaed9c5f4c5b59b"], + "scripts/macos_managed_boundary/control.py": ["100644", "10eff5bf409d03071d204e43f554ed23313d5cb7"], + "scripts/macos_managed_boundary/control_admission.py": ["100644", "92ffe60f0bccfc524a1f1ede7ef782488ac0e147"], + "scripts/macos_managed_boundary/control_broker.c": ["100644", "5dfa0384ac32fc589d6fd6902f03a70e91b53211"], + "scripts/macos_managed_boundary/control_build.py": ["100644", "055369c3383ced9fcdb8da896757bbccedb374fa"], + "scripts/macos_managed_boundary/control_events_test.c": ["100644", "d9b0f743c706bd80a5b23d1bf54078c88ef6f443"], + "scripts/macos_managed_boundary/control_exec.py": ["100644", "e9bbd85de0560e12e6755f9d1ad6222393c04c20"], + "scripts/macos_managed_boundary/control_exec_policy.h": ["100644", "57d899bf561fc75e4a5e6a7b6e0bf05aa1760efc"], + "scripts/macos_managed_boundary/control_exec_test.c": ["100644", "567d40b76524b2502ae5f174aebe7174e2635a1a"], + "scripts/macos_managed_boundary/control_mach.inc": ["100644", "9927fb7e10a4490a40caf4a2b0779e523ae993ac"], + "scripts/macos_managed_boundary/control_mach_build.py": ["100644", "14a15da718dc40c590eaf1860273b94b3dd30668"], + "scripts/macos_managed_boundary/control_mach_policy.h": ["100644", "0919b2fbdcee24fc50290bf14c57213845d92979"], + "scripts/macos_managed_boundary/control_mach_reply.h": ["100644", "1693abb3a73ccd9d2259c6f94a3bf00a1fbe9693"], + "scripts/macos_managed_boundary/control_mach_reply_test.c": ["100644", "ba8b8cbf1adae6b41bc3e39a641ed2e916cd02e4"], + "scripts/macos_managed_boundary/control_mach_test.c": ["100644", "ff3314f2bb15b1857acf351b6970f44248e88b59"], + "scripts/macos_managed_boundary/control_probes.h": ["100644", "1cf42bac67a10f549bbe7d57a468657fc60b7cd0"], + "scripts/macos_managed_boundary/control_resource.h": ["100644", "cb1450af1c9f1f528dfeac5525abd4bdd0876b23"], + "scripts/macos_managed_boundary/control_socket.py": ["100644", "1a8b73de227f810995a013a4a7d309ff6e9e79d0"], + "scripts/macos_managed_boundary/control_state.py": ["100644", "14512bd7d9f7aec04d118374b86c2d64bcb12582"], + "scripts/macos_managed_boundary/control_target.c": ["100644", "c0e88cdda17dedfc806ff1f700d01bd8bb040d42"], + "scripts/macos_managed_boundary/control_timers_test.c": ["100644", "6e643eb69df610b70e8e023893d2c0c98741e02b"], + "scripts/macos_managed_boundary/control_worker.c": ["100644", "71b7304e9c6cc10967ae4fb7d56678bf8dc9bb67"], + "scripts/macos_managed_boundary/fixture.sb": ["100644", "dd1a11b12fbebfedc561d8c304459c689faa2cc3"], + "scripts/macos_managed_boundary/managed_subprocess.py": ["100644", "245163c9102539e28de8662a53ee473513abd98e"], + "scripts/macos_managed_boundary/preflight.py": ["100644", "675b1f76c12d8a5b502c0ea73c4b51f8c8d0d139"], + "scripts/macos_managed_boundary/project_acquisition.py": ["100644", "d4dff6aef45967189beab3740002293762f4b880"], + "scripts/macos_managed_boundary/project_corpus.py": ["100644", "7a9cd4875d6b6780ca42f6df69deaf0460d4f110"], + "scripts/macos_managed_boundary/python_analyzer_worker.py": ["100644", "aee1df2c771e7fb461ae30b35eca63e22c28b175"], + "scripts/macos_managed_boundary/python_analyzers.py": ["100644", "fdb5a0a08ab087e77d13180c3a8443a2a336d833"], + "scripts/macos_managed_boundary/python_candidate.c": ["100644", "e9dcffb3a52d987e55331df0d31cdd2122b1b6de"], + "scripts/macos_managed_boundary/python_candidate.py": ["100644", "b02be074101142f07605766c5a33bbeabd85ef2d"], + "scripts/macos_managed_boundary/resource_limits.c": ["100644", "3b3f43548d75e514b4cb64819c2333befdbb08eb"], + "scripts/macos_managed_boundary/resource_limits.py": ["100644", "f654ef6a91e3b248e70b89863f9ee616eabd293a"], + "scripts/macos_managed_boundary/startup.py": ["100644", "0ccc729d4949454b640c596d863cf04a2e5023eb"], + "scripts/macos_managed_boundary/startup_broker.c": ["100644", "38c538c47df1e691cec79a60f440e9303a21a425"], + "scripts/macos_managed_boundary/startup_observe.c": ["100644", "998557afa562fe50d13b91b4571abbf2285d073b"], + "scripts/macos_managed_boundary/startup_worker.c": ["100644", "efc23f8cc4734f27772634efe6a735088c55e1c8"], + "scripts/native_analyzer_inputs/README.md": ["100644", "500b5a5483e0c5a6b359404371058cd3e441f6e0"], + "scripts/native_analyzer_inputs/Z3-LICENSE.txt": ["100644", "cc90bed7477d0809f4309b718e920d411e1f3da8"], + "scripts/native_analyzer_inputs/candidate-version-policy.json": ["100644", "1a4f327080adfb22b3b5d331eb4d7ac35b698ec0"], + "scripts/native_analyzer_inputs/candidate_policy.py": ["100644", "b9be616480a964310fb6d85d34a71b0deb8feac1"], + "scripts/native_analyzer_inputs/darwin-arm64-cp311.txt": ["100644", "ed828d97fbd62ec16fb2e3cd7c922d245ec2b3fd"], + "scripts/native_analyzer_inputs/darwin-arm64-cp312.txt": ["100644", "ed828d97fbd62ec16fb2e3cd7c922d245ec2b3fd"], + "scripts/native_analyzer_inputs/darwin-arm64-cp313.txt": ["100644", "ed828d97fbd62ec16fb2e3cd7c922d245ec2b3fd"], + "scripts/native_analyzer_inputs/requirements.in": ["100644", "b78f1909dc19fb56c0002f8e62fd16cc04bec228"], + "scripts/native_analyzer_inputs/z3-license-provenance.json": ["100644", "0823981d25df6c756efcc61284a59c5ea9b1fd35"], + "scripts/native_capsule_cache.py": ["100644", "193384434a6b67e913d43a7b6f53ab57b6e6dadc"], + "scripts/native_release/acceptance.py": ["100644", "115efd21a32b6d464e3f175b444a77f74d32d43b"], + "scripts/native_release/artifacts.py": ["100644", "9970817a5276ebef0019da59ab4060fcd078e474"], + "scripts/native_release/boundary.py": ["100644", "7f8e717f9217040e4a61f956fc661a7098fb496f"], + "scripts/native_release/build.py": ["100644", "f0d0dbd346e47c9c9352a2d299d64ff1cbfe92b9"], + "scripts/native_release/cli.py": ["100644", "4b7053e112a63e20dcf0768608606aa86ca36412"], + "scripts/native_release/environment.py": ["100644", "55ee33a02a858dbfe59457fa2260dec4efa885e5"], + "scripts/native_release/module-signing-public.pem": ["100644", "300cb0570842084c871de1bcbffc1ad059c0c16f"], + "scripts/native_release/platforms.py": ["100644", "6a03d2a29cad912c0574ce210d2e97ed09c98a58"], + "scripts/native_release/prepare-ci.sh": ["100755", "f096150b30c36f3f1951129e9c2272f89e3b55c9"], + "scripts/native_release/publish.py": ["100644", "ce26b3da049d884bf55c6251d535f69c78f5974b"], + "scripts/native_release/release.py": ["100644", "dd66b279e542daf9fdf3003aab5aa8ea590ca662"], + "tests/fixtures/portable-runtime/corpus.json": ["100644", "ce3167530e38721dfbe21e0d310ae2aef72499ef"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/.python-version": ["100644", "ed7d51a38eca5f70d3e1964fd3e9710dd51de48d"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/README.md": ["100644", "f6d4573f25b082afe100665c6a5e3bfa917d6160"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/hatch.toml": ["100644", "3d24faf5a5263e8b8977fa1937bd62dea4acbe74"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/lint_probe.py.in": ["100644", "9673111d8e7bb4a32dae498a81d5db5e96da5d7e"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/pyproject.toml": ["100644", "c02c8d80f20ead947d2865995aae2fb8f70747ed"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/src/portable_customer/__init__.py.in": ["100644", "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/src/portable_customer/core.py.in": ["100644", "dc33fb4ae9c82e5d6c3ad07e2de11edb29bbdbfe"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/tests/test_core.py.in": ["100644", "4db4275cdadf76b12253f9c348bf8ff69057bfe8"], + "tests/fixtures/portable-runtime/hatch-bootstrap-companion/tools/fixture_value.py.in": ["100644", "b11ef08a37024e91197111270d85f90e192ed482"], + "tests/fixtures/portable-runtime/hatch-detached/README.md": ["100644", "dc351c0922a64b617707bb9a56157aea36e07b13"], + "tests/fixtures/portable-runtime/hatch-detached/hatch.toml": ["100644", "d0a16e66d45df851c2f4f5b06dad4446e84f0609"], + "tests/fixtures/portable-runtime/hatch-detached/pyproject.toml": ["100644", "f961711ed330bdb79a36ad0d231bfddc2c1266cc"], + "tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/__init__.py.in": ["100644", "539b71d67ffcf457e4f708d85bf456d39bd1716c"], + "tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/core.py.in": ["100644", "737a5a1464fb9a4352727d5379e79057303be217"], + "tests/fixtures/portable-runtime/hatch-detached/tests/test_core.py.in": ["100644", "dda44e8c8f53f07178d5fa53acaf2364419079fa"], + "tests/fixtures/portable-runtime/hatch-detached/tools/fixture_value.py.in": ["100644", "cb05742b435b396b06970ba2a091ad0decdb63e3"], + "tests/host/proof_capsule_deferred_review_ci.py": ["100644", "02f126711d39d384c188921972fa0b639af872a5"], + "tests/native/proof_macos_managed_uv_child.py": ["100644", "f598b7fa33fd66b37b85a3bc29f9782c65e086dc"], + "tests/native/proof_macos_native_broker_wait.py": ["100644", "7721d86578ccba860390a551c58260eb24002709"], + "tests/native/proof_python_candidate_matrix.py": ["100644", "450d09cc75858841c6a72520503be340c7cfeb2b"], + "tests/unit/specfact_code_review/run/test_native_capsule.py": ["100644", "b50556761ab330bd0b829e98bbc9d349aa1df46a"], + "tests/unit/specfact_code_review/test_capsule_tool_commands.py": ["100644", "ae4df22d672f6e8ba422f5acf37323f5354ce249"], + "tests/unit/test_assemble_macos_native_capsule.py": ["100644", "f1484d94734b281ecdcd91c96fe7341168a21a2c"], + "tests/unit/test_build_macos_native_capsule.py": ["100644", "80b4f68ff16875f8143c6ecc376162a56c45bb7a"], + "tests/unit/test_capsule_analyzer_diagnostics.py": ["100644", "80b6fc6b4e067f6468190b9dee48c7139f0abe88"], + "tests/unit/test_capsule_customer_gate.py": ["100644", "c55dfbc228b8e6e1452a9696943300c4b8d34188"], + "tests/unit/test_capsule_deferred_review_ci.py": ["100644", "bcbcd3a1b86dd759f4800561a7c7acc9cb31d1ef"], + "tests/unit/test_capsule_profile_summary.py": ["100644", "693888c42c82ff0c05b7849c49fc3fd4fafdf6ab"], + "tests/unit/test_capsule_proof_contexts.py": ["100644", "407bb58c244b198b5e9afdb3a02c109081ae28ec"], + "tests/unit/test_capsule_refresh_lock.py": ["100644", "72d68cd5088c6e982200a1ff408e2c10b1112209"], + "tests/unit/test_capsule_review_projection.py": ["100644", "7585292c2612d442e68801ce729755c94bc34dc4"], + "tests/unit/test_external_capsule_corpus.py": ["100644", "abbc7447db0bd82032e956dfaaa14bf43ff39cfd"], + "tests/unit/test_macos_capsule_candidate.py": ["100644", "9ba838c2186808e2745039c1c80b83f9f22ad0d0"], + "tests/unit/test_native_capsule_cache.py": ["100644", "ede75440fe1d4c948fd0d43225d0819dfa4bddea"] + } +} diff --git a/packages/specfact-codebase/module-package.yaml b/packages/specfact-codebase/module-package.yaml index 09cbe33d..43f51431 100644 --- a/packages/specfact-codebase/module-package.yaml +++ b/packages/specfact-codebase/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-codebase -version: 0.41.16 +version: 0.42.1 commands: - code tier: official @@ -13,6 +13,7 @@ pip_dependencies: - click - icontract - pydantic + - packaging - pyyaml - rich - ruamel.yaml @@ -24,5 +25,5 @@ description: Official SpecFact codebase bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:c8ea244c3675b9c6a322046fbc9d179f2e943bd9d4a65dc4fb66bd0bf843d660 - signature: C6nqNXzaW6i+BpyUHWKnnR8qz2K8wT9lpntV/4NfSF0UcayVzWPWO2vfRU/04W3YsY1eOwrhQOtoaRvFMAJ/Cg== + checksum: sha256:a826c75ef57409191711f1af03dd13877d56d48769ba3a2e29d1929e16ce9909 + signature: ObxxUEVs2685w9pdX5q1smbZAxuVYuFsEIJ0R25NZoK/Sq8ur+8Yqk5xEvJGWpMuzMzr9zq2RJbtTKrd++LIAQ== diff --git a/packages/specfact-codebase/src/specfact_codebase/project_runtime.py b/packages/specfact-codebase/src/specfact_codebase/project_runtime.py new file mode 100644 index 00000000..6dab3383 --- /dev/null +++ b/packages/specfact-codebase/src/specfact_codebase/project_runtime.py @@ -0,0 +1,676 @@ +"""Trusted local Hatch preparation and execution without dependency synchronization. + +This first slice supports detached virtual environments and explicit executable argv. +Native Hatch scripts and effective environment-context expansion are not implemented. +""" + +from __future__ import annotations + +import contextlib +import hashlib +import json +import os +import re +import selectors +import shutil +import signal +import subprocess +import tempfile +import time +import tomllib +from collections.abc import Mapping +from dataclasses import dataclass, field +from pathlib import Path +from typing import IO, Literal + +from packaging.specifiers import InvalidSpecifier, SpecifierSet + + +_OUTPUT_LIMIT = 65536 +_HATCH_CONFIG_DIAGNOSTIC = ( + "Custom HATCH_CONFIG global configuration is unsupported in this milestone; unset it before inspection" +) +_RESERVED_ENV = frozenset({"PATH", "VIRTUAL_ENV", "PYTHONHOME", "PYTHONPATH", "CONDA_PREFIX", "__PYVENV_LAUNCHER__"}) +_IGNORED_DIRS = frozenset( + {".git", ".venv", "venv", "__pycache__", ".pytest_cache", ".ruff_cache", ".mypy_cache", "dist", "build", ".hatch"} +) +_CONFIG_NAMES = frozenset( + { + "pyproject.toml", + "hatch.toml", + ".python-version", + "pytest.ini", + ".pytest.ini", + "pytest.toml", + ".pytest.toml", + ".coveragerc", + "tox.ini", + "setup.cfg", + "setup.py", + "ruff.toml", + ".ruff.toml", + "uv.lock", + "poetry.lock", + "pylock.toml", + } +) + + +@dataclass(frozen=True) +class ProjectSelection: + python: str | None = None + manager: str | None = None + environment: str | None = None + groups: tuple[str, ...] = () + extras: tuple[str, ...] = () + hatch: str = "hatch" + + +@dataclass(frozen=True) +class ProjectCommand: + argv: tuple[str, ...] + cwd: Path + env_overrides: Mapping[str, str] = field(default_factory=dict) + + +@dataclass(frozen=True) +class ProjectPlan: + root: Path + selection: ProjectSelection + input_identity: str + environment: str | None + python_request: str | None + python_constraint: str + diagnostics: tuple[str, ...] + hatch_environment: Mapping[str, object] = field(default_factory=dict, repr=False) + + +@dataclass(frozen=True) +class InterpreterFacts: + executable: Path + prefix: Path + base_prefix: Path + version: tuple[int, int, int] + scripts: Path + + +@dataclass(frozen=True) +class EnvironmentIdentity: + interpreter: InterpreterFacts + manager: str + manager_version: str + environment: str + ownership: Literal["existing", "invocation"] + inputs: str + + +@dataclass(frozen=True) +class PreparationFailure: + plan: ProjectPlan + reason: str + remedy: str + + +@dataclass(frozen=True) +class ProjectCommandResult: + outcome: Literal["exited", "unavailable", "timed_out", "execution_failed"] + exit_code: int | None + stdout: str = "" + stderr: str = "" + output_truncated: bool = False + source_identity: str = "" + + +def _files(root: Path): + pending = [root] + while pending: + directory = pending.pop() + with os.scandir(directory) as stream: + entries = sorted(stream, key=lambda entry: entry.name) + directories = [] + for entry in entries: + if not entry.is_dir(): + yield Path(entry.path) + continue + if entry.name in _IGNORED_DIRS or entry.name.endswith(".egg-info"): + continue + if entry.is_symlink(): + yield Path(entry.path) + continue + directories.append(Path(entry.path)) + pending.extend(reversed(directories)) + + +def _identity(root: Path, *, source: bool = False) -> str: + digest = hashlib.sha256() + for path in _files(root): + if path.is_symlink() and path.is_dir(): + digest.update(json.dumps(["directory-link", str(path.relative_to(root)), os.readlink(path)]).encode()) + continue + selected = ( + path.name in _CONFIG_NAMES + or path.name.startswith(("requirements", "constraints")) + or path.suffix == ".lock" + ) + if selected or (source and path.suffix in {".py", ".in"}): + digest.update( + json.dumps([str(path.relative_to(root)), hashlib.sha256(path.read_bytes()).hexdigest()]).encode() + ) + return digest.hexdigest() + + +def _version_components(request: str) -> tuple[int, ...] | None: + parts = request.split(".") + valid = 1 <= len(parts) <= 3 and all(part.isdigit() for part in parts) + return tuple(map(int, parts)) if valid else None + + +@dataclass +class _Inspection: + environment: str | None + python_request: str | None = None + python_constraint: str = "" + hatch_environment: Mapping[str, object] = field(default_factory=dict) + diagnostics: list[str] = field(default_factory=list) + + +def _select_environment(envs: object, state: _Inspection) -> None: + if not isinstance(envs, dict): + raise ValueError("Hatch envs must be a table") + if state.environment is None: + if len(envs) == 1: + state.environment = next(iter(envs)) + else: + state.diagnostics.append("Choose environment explicitly from: " + ", ".join(sorted(envs))) + if state.environment is not None: + if state.environment not in envs or not isinstance(envs[state.environment], dict): + state.diagnostics.append( + f"Unknown environment {state.environment!r}; candidates: {', '.join(sorted(envs))}" + ) + else: + state.hatch_environment = envs[state.environment] + + +def _check_environment(state: _Inspection) -> None: + config = state.hatch_environment + if config.get("type", "virtual") != "virtual": + state.diagnostics.append("Selected environment type is unsupported; select standard virtual") + if config.get("detached") is not True: + state.diagnostics.append("Selected environment must be detached in this milestone") + for key in ("env-vars", "env-include", "env-exclude", "matrix", "template", "platforms"): + if config.get(key): + state.diagnostics.append( + f"Selected Hatch {key} requires native context fidelity not supported in this milestone" + ) + + +def _read_python_request(root: Path, state: _Inspection) -> None: + raw_python = state.hatch_environment.get("python") + if raw_python is None or isinstance(raw_python, str): + state.python_request = raw_python + else: + state.diagnostics.append("Selected Python declaration must be a string") + version_file = root / ".python-version" + if not version_file.exists(): + return + declared = version_file.read_text().strip() + if state.python_request and not os.path.isabs(state.python_request): + hatch_parts = _version_components(state.python_request) + file_parts = _version_components(declared) + width = min(len(hatch_parts or ()), len(file_parts or ())) + if not hatch_parts or not file_parts or hatch_parts[:width] != file_parts[:width]: + state.diagnostics.append( + "Selected Hatch Python conflicts with .python-version; choose a consistent declaration" + ) + state.python_request = declared + + +def _read_declarations(root: Path, selection: ProjectSelection, state: _Inspection) -> None: + project = tomllib.loads((root / "pyproject.toml").read_text()) + state.python_constraint = project.get("project", {}).get("requires-python", "") + SpecifierSet(state.python_constraint) + hatch_file = root / "hatch.toml" + hatch = tomllib.loads(hatch_file.read_text()) if hatch_file.exists() else project.get("tool", {}).get("hatch", {}) + envs = hatch.get("envs", {"default": {}}) + _select_environment(envs, state) + if selection.manager not in {None, "hatch"}: + state.diagnostics.append("Only native Hatch is supported in this milestone") + if selection.groups or selection.extras: + state.diagnostics.append("Explicit groups/extras are not supported in this Hatch slice") + if hatch.get("env", {}).get("requires"): + state.diagnostics.append( + "Hatch environment plugin requirements are unsupported; provision a standard virtual environment" + ) + if state.environment is not None and state.environment in envs: + _check_environment(state) + _read_python_request(root, state) + + +def inspect_project(root: Path, selection: ProjectSelection | None = None) -> ProjectPlan: + """Inspect declarations only. Ambiguities and unsupported cells remain diagnostics.""" + selection = selection or ProjectSelection() + root = root.resolve() + state = _Inspection(selection.environment) + if os.environ.get("HATCH_CONFIG"): + state.diagnostics.append(_HATCH_CONFIG_DIAGNOSTIC) + try: + _read_declarations(root, selection, state) + except (OSError, ValueError, TypeError, AttributeError, InvalidSpecifier) as error: + state.diagnostics.append(f"Cannot inspect project declarations: {error}") + input_identity = "" + try: + input_identity = _identity(root) + except OSError as error: + state.diagnostics.append(f"Cannot fingerprint project declarations: {error}") + return ProjectPlan( + root, + selection, + input_identity, + state.environment, + state.python_request, + state.python_constraint, + tuple(state.diagnostics), + state.hatch_environment, + ) + + +def _environment(scripts: Path | None = None) -> dict[str, str]: + env = { + key: value + for key, value in os.environ.items() + if key not in _RESERVED_ENV + and not key.startswith("PYTHON") + and (not key.startswith(("HATCH_", "CONDA_")) or key in {"HATCH_DATA_DIR", "HATCH_CACHE_DIR"}) + } + env["PATH"] = os.pathsep.join([str(scripts)] if scripts else []) + (os.pathsep if scripts else "") + os.defpath + return env + + +def _register_pipe(selector: selectors.BaseSelector, stream: IO[bytes] | None, index: int) -> None: + if stream is None: + raise ValueError("Subprocess capture pipe is unavailable") + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, index) + + +def _read_output(selector, buffers, truncated, wait: float) -> bool: + events = selector.select(wait) + for key, _ in events: + block = os.read(key.fd, 8192) + if not block: + selector.unregister(key.fileobj) + continue + index = key.data + available = _OUTPUT_LIMIT - len(buffers[index]) + buffers[index].extend(block[:available]) + truncated[index] |= len(block) > available + return bool(events) + + +def _capture_output(process, selector, timeout: float): + buffers = [bytearray(), bytearray()] + truncated = [False, False] + deadline = time.monotonic() + timeout + outcome: Literal["exited", "timed_out"] = "exited" + while selector.get_map() or process.poll() is None: + remaining = deadline - time.monotonic() + if remaining <= 0: + if process.poll() is None: + outcome = "timed_out" + break + events = _read_output(selector, buffers, truncated, min(remaining, 0.05)) + if not events and process.poll() is not None: + break + return outcome, buffers, truncated + + +def _stop_process(process) -> None: + # Detached descendants can retain pipes beyond the original process group. + with contextlib.suppress(ProcessLookupError): + os.killpg(process.pid, signal.SIGKILL) + process.wait() + + +def _execute(argv: tuple[str, ...], cwd: Path, env: Mapping[str, str], timeout: float) -> ProjectCommandResult: + if timeout <= 0: + return ProjectCommandResult("execution_failed", None, stderr="Timeout must be positive") + try: + with ( + subprocess.Popen( + argv, cwd=cwd, env=dict(env), stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True + ) as process, + selectors.DefaultSelector() as selector, + ): + for index, stream in enumerate((process.stdout, process.stderr)): + _register_pipe(selector, stream, index) + try: + outcome, buffers, truncated = _capture_output(process, selector, timeout) + finally: + _stop_process(process) + return ProjectCommandResult( + outcome, + process.returncode if outcome == "exited" else None, + buffers[0].decode(errors="replace"), + buffers[1].decode(errors="replace"), + any(truncated), + ) + except FileNotFoundError as error: + return ProjectCommandResult("unavailable", None, stderr=str(error)) + except (OSError, ValueError) as error: + return ProjectCommandResult("execution_failed", None, stderr=str(error)) + + +def _probe(executable: Path, cwd: Path) -> InterpreterFacts: + code = ( + "import json,sys,sysconfig; print(json.dumps([sys.executable,sys.prefix,sys.base_prefix," + "list(sys.version_info[:3]),sysconfig.get_path('scripts')]))" + ) + result = _execute((str(executable), "-I", "-c", code), cwd, _environment(), 15) + if result.outcome != "exited" or result.exit_code != 0: + raise ValueError(f"Selected Python probe failed: {result.stderr}") + try: + executable_name, prefix, base_prefix, version, scripts = json.loads(result.stdout) + if len(version) != 3 or not all(isinstance(part, int) for part in version): + raise ValueError("invalid version") + return InterpreterFacts(Path(executable_name), Path(prefix), Path(base_prefix), tuple(version), Path(scripts)) + except (ValueError, TypeError) as error: + raise ValueError("Selected Python returned malformed identity") from error + + +def _python(plan: ProjectPlan) -> InterpreterFacts: + declared = plan.hatch_environment.get("python") + absolute_declared = declared if isinstance(declared, str) and os.path.isabs(declared) else None + request = plan.selection.python or absolute_declared or plan.python_request + if not request: + raise ValueError( + "Select a local project Python explicitly or declare .python-version; controller fallback is disabled" + ) + if os.path.isabs(request): + executable = Path(request).absolute() + else: + version = re.fullmatch(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", request) + name = "python" + ".".join(part for part in version.groups()[:2] if part) if version else request + found = shutil.which(name) + if not found: + raise ValueError(f"Project Python {request} is not available locally; select its absolute executable") + executable = Path(found).absolute() + facts = _probe(executable, plan.root) + _validate_explicit_request(facts, plan.selection.python) + _validate_python(facts, plan) + return facts + + +def _validate_explicit_request(facts: InterpreterFacts, request: str | None) -> None: + parts = _version_components(request) if request else None + if parts and facts.version[: len(parts)] != parts: + raise ValueError(f"Selected Python {facts.version} does not match explicitly requested {request}") + + +def _validate_python(facts: InterpreterFacts, plan: ProjectPlan) -> None: + if not SpecifierSet(plan.python_constraint).contains(".".join(map(str, facts.version))): + raise ValueError(f"Selected Python {facts.version} violates project requires-python {plan.python_constraint}") + declarations = { + request + for request in (plan.python_request, plan.hatch_environment.get("python")) + if isinstance(request, str) and request + } + for request in sorted(declarations): + if os.path.isabs(request): + declared = _probe(Path(request), plan.root) + if facts.base_prefix.resolve() != declared.base_prefix.resolve() or facts.version != declared.version: + raise ValueError( + f"Selected Python {facts.version} at {facts.base_prefix} " + f"does not match declared Hatch Python {request}" + ) + else: + parts = _version_components(request) + if parts is None or facts.version[: len(parts)] != parts: + raise ValueError(f"Selected Python {facts.version} does not match {request}") + + +def _execution_failure(result: ProjectCommandResult, diagnostic: str, source: str) -> ProjectCommandResult: + stderr = (diagnostic + "\n" + result.stderr).encode() + return ProjectCommandResult( + "execution_failed", + None, + result.stdout, + stderr[:_OUTPUT_LIMIT].decode(errors="replace"), + result.output_truncated or len(stderr) > _OUTPUT_LIMIT, + source, + ) + + +def _command_admission(command: ProjectCommand, cwd: Path, root: Path, source: str) -> ProjectCommandResult | None: + if not cwd.is_relative_to(root) or not cwd.is_dir() or not command.argv: + return ProjectCommandResult( + "execution_failed", + None, + stderr="Command needs executable-first argv and a project working directory", + source_identity=source, + ) + if _RESERVED_ENV.intersection(command.env_overrides) or any( + not isinstance(key, str) or not isinstance(value, str) or "=" in key or "\0" in key + value + for key, value in command.env_overrides.items() + ): + return ProjectCommandResult( + "execution_failed", + None, + stderr="Invalid or identity-reserved environment override", + source_identity=source, + ) + return None + + +def _command_executable(command: ProjectCommand, facts: InterpreterFacts, source: str) -> Path | ProjectCommandResult: + name = command.argv[0] + if name == "python": + return facts.executable + if Path(name).name == name and name not in {".", ".."}: + return facts.scripts / name + if Path(name).is_absolute() and (Path(name) == facts.executable or Path(name).parent == facts.scripts): + return Path(name) + return ProjectCommandResult( + "unavailable", None, stderr="Executable must belong to the selected environment", source_identity=source + ) + + +@dataclass(frozen=True) +class PreparedProject: + plan: ProjectPlan + identity: EnvironmentIdentity + + def run(self, command: ProjectCommand, *, timeout: float) -> ProjectCommandResult: + """Run in the selected environment, with no manager lifecycle authority.""" + try: + source = _identity(self.plan.root, source=True) + inputs = _identity(self.plan.root) + except OSError as error: + return ProjectCommandResult( + "execution_failed", None, stderr=f"Cannot fingerprint project before execution: {error}" + ) + if inputs != self.plan.input_identity: + return ProjectCommandResult( + "execution_failed", + None, + stderr="Project configuration/dependencies changed; reinspect before execution", + source_identity=source, + ) + cwd = command.cwd.resolve() + refusal = _command_admission(command, cwd, self.plan.root, source) + if refusal is not None: + return refusal + facts = self.identity.interpreter + name = command.argv[0] + executable = _command_executable(command, facts, source) + if isinstance(executable, ProjectCommandResult): + return executable + if not executable.is_file() or not os.access(executable, os.X_OK): + return ProjectCommandResult( + "unavailable", + None, + stderr=( + f"Selected environment has no executable {name!r}; " + "install it through the project manager in a separate preparation" + ), + source_identity=source, + ) + env = _environment(facts.scripts) + env["VIRTUAL_ENV"] = str(facts.prefix) + env.update(command.env_overrides) + result = _execute((str(executable), *command.argv[1:]), cwd, env, timeout) + try: + inputs = _identity(self.plan.root) + except OSError as error: + return _execution_failure(result, f"Cannot fingerprint project after execution: {error}", source) + if inputs != self.plan.input_identity: + return _execution_failure( + result, "Project configuration/dependencies changed during execution; reinspect", source + ) + return ProjectCommandResult( + result.outcome, result.exit_code, result.stdout, result.stderr, result.output_truncated, source + ) + + +def _check_preparation(plan: ProjectPlan, workspace: Path) -> None: + if plan.diagnostics: + raise ValueError("; ".join(plan.diagnostics)) + if os.environ.get("HATCH_CONFIG"): + raise ValueError(_HATCH_CONFIG_DIAGNOSTIC) + if _identity(plan.root) != plan.input_identity: + raise ValueError("Project inputs changed; reinspect before preparation") + if workspace.is_relative_to(plan.root) or plan.root.is_relative_to(workspace): + raise ValueError("Preparation workspace must be external to the project") + if workspace.exists(): + raise ValueError("Preparation workspace must be fresh and invocation-owned") + + +def _check_selected_target(requested: InterpreterFacts, target: Path, existing: bool, workspace: Path) -> None: + if workspace.is_relative_to(requested.prefix) or (existing and workspace.is_relative_to(target.resolve())): + raise ValueError("Preparation workspace must be outside the selected interpreter prefix") + if requested.prefix != requested.base_prefix and (not existing or requested.prefix.resolve() != target.resolve()): + raise ValueError("Explicit virtualenv is not the selected native Hatch environment") + + +@dataclass(frozen=True) +class _HatchTarget: + executable: str + version: ProjectCommandResult + path: Path + environment: dict[str, str] + existing: bool + + +def _find_hatch_target(plan: ProjectPlan) -> _HatchTarget: + hatch = shutil.which(plan.selection.hatch) + if not hatch: + raise ValueError("Native Hatch executable unavailable; install Hatch externally and select its path") + env = _environment(Path(hatch).parent) + version = _execute((hatch, "--version"), plan.root, env, 15) + if version.outcome != "exited" or version.exit_code: + raise ValueError(f"Native Hatch version query failed: {version.stderr}") + location = _execute((hatch, "env", "find", str(plan.environment)), plan.root, env, 30) + if location.outcome != "exited" or location.exit_code: + raise ValueError(f"Native Hatch environment lookup failed: {location.stderr}") + target = Path(location.stdout.strip()) + if not target.is_absolute(): + raise ValueError("Native Hatch returned an invalid environment path") + existing = (target / "bin/python").is_file() + return _HatchTarget(hatch, version, target, env, existing) + + +def _create_cold_target(plan: ProjectPlan, native: _HatchTarget, workspace: Path) -> Path: + if plan.hatch_environment.get("locked") or plan.hatch_environment.get("path"): + raise ValueError( + "Cold selected locked/path environment is unsupported until immutable native preparation is proved" + ) + target = workspace / "environment" + native.environment["HATCH_ENV_TYPE_VIRTUAL_PATH"] = str(target) + creation = _execute((native.executable, "env", "create", str(plan.environment)), plan.root, native.environment, 180) + if creation.outcome != "exited" or creation.exit_code: + raise ValueError(f"Native Hatch preparation {creation.outcome}: {creation.stderr}") + return target + + +def _check_prepared_identity(facts: InterpreterFacts, requested: InterpreterFacts, target: Path) -> None: + if ( + facts.prefix.resolve() != target.resolve() + or facts.version != requested.version + or facts.base_prefix.resolve() != requested.base_prefix.resolve() + ): + raise ValueError( + f"Prepared interpreter {facts.version} at {facts.prefix} with base {facts.base_prefix} " + f"does not match selected Python {requested.version} with base {requested.base_prefix} " + f"at environment {target}" + ) + + +def _publish_completion(plan: ProjectPlan, identity: EnvironmentIdentity, hatch: str, workspace: Path) -> None: + facts = identity.interpreter + payload = { + "root": str(plan.root), + "environment": plan.environment, + "inputs": plan.input_identity, + "python": str(facts.executable), + "prefix": str(facts.prefix), + "version": facts.version, + "hatch": hatch, + "hatch_version": identity.manager_version, + "ownership": identity.ownership, + } + with tempfile.NamedTemporaryFile(mode="w", dir=workspace, delete=False) as stream: + json.dump(payload, stream) + temporary = Path(stream.name) + temporary.replace(workspace / "completed.json") + + +def _preparation_failure(plan: ProjectPlan, workspace: Path, owned: bool, error: Exception) -> PreparationFailure: + remedy = ( + "Resolve the diagnostic and reinspect with an explicit compatible local environment/Python; " + "use a fresh external workspace" + ) + if owned: + try: + shutil.rmtree(workspace) + except OSError as cleanup_error: + remedy += f". Incomplete workspace remains at {workspace}; cleanup failed: {cleanup_error}" + return PreparationFailure(plan, str(error), remedy) + + +def prepare_project(plan: ProjectPlan, workspace: Path) -> PreparedProject | PreparationFailure: + """Resolve existing Hatch without sync, or natively create one owned cold target.""" + owned = False + workspace = workspace.resolve() + try: + _check_preparation(plan, workspace) + requested = _python(plan) if plan.selection.python else None + if requested is not None and workspace.is_relative_to(requested.prefix): + raise ValueError("Preparation workspace must be outside the selected interpreter prefix") + native = _find_hatch_target(plan) + target = native.path + if requested is None: + requested = _probe(target / "bin/python", plan.root) if native.existing else _python(plan) + _validate_python(requested, plan) + _check_selected_target(requested, target, native.existing, workspace) + native.environment["PATH"] = str(requested.scripts) + os.pathsep + native.environment["PATH"] + native.environment["HATCH_PYTHON"] = str(requested.executable) + workspace.mkdir(parents=True, mode=0o700) + owned = True + if not native.existing: + target = _create_cold_target(plan, native, workspace) + facts = _probe(target / "bin/python", plan.root) + _validate_python(facts, plan) + _check_prepared_identity(facts, requested, target) + if _identity(plan.root) != plan.input_identity: + raise ValueError("Project inputs changed during preparation; reinspect") + identity = EnvironmentIdentity( + facts, + "hatch", + native.version.stdout.strip(), + str(plan.environment), + "existing" if native.existing else "invocation", + plan.input_identity, + ) + _publish_completion(plan, identity, native.executable, workspace) + return PreparedProject(plan, identity) + except (OSError, ValueError, TypeError) as error: + return _preparation_failure(plan, workspace, owned, error) diff --git a/scripts/check-bundle-imports.py b/scripts/check-bundle-imports.py index a5078c56..413abb83 100755 --- a/scripts/check-bundle-imports.py +++ b/scripts/check-bundle-imports.py @@ -2,9 +2,8 @@ # ruff: noqa: N999 """Enforce bundle import boundaries for specfact-cli-modules.""" -from __future__ import annotations - import ast +import subprocess import sys from dataclasses import dataclass from pathlib import Path @@ -183,6 +182,9 @@ def main() -> int: violations.extend(_scan_python_file(bundle_name, file_path)) if not violations: + scope = subprocess.run([sys.executable, str(ROOT / "scripts/check_native_review_scope.py")], check=False) + if scope.returncode != 0: + return 1 sys.stdout.write("Import boundary check passed.\n") return 0 diff --git a/scripts/check_native_review_scope.py b/scripts/check_native_review_scope.py new file mode 100644 index 00000000..40d42e6c --- /dev/null +++ b/scripts/check_native_review_scope.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import fnmatch +import hashlib +import json +import os +import re +import stat +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +POLICY_SHA256 = "1d9696307907e1af6985ae5e41a4a453a1e5ce9bcd6662b5855d795893ba8cef" + + +def read_policy(root: Path) -> tuple[str, tuple[str, ...], dict[str, list[str]]]: + policy_bytes = (root / "openspec/native-review-policy.json").read_bytes() + if hashlib.sha256(policy_bytes).hexdigest() != POLICY_SHA256: + raise ValueError("migration policy changed: execution_backend, freeze_ref and legacy_paths require review") + policy = json.loads(policy_bytes) + if not isinstance(policy, dict) or policy.get("execution_backend") != "trusted-local-native": + raise ValueError("execution_backend must remain trusted-local-native; the owner already decided this") + baseline = policy.get("freeze_ref") + if not isinstance(baseline, str) or not re.fullmatch(r"[0-9a-f]{40,64}", baseline): + raise ValueError("freeze_ref must name the recorded migration baseline") + patterns = policy_patterns(policy) + manifest = policy.get("baseline_files") + if not isinstance(manifest, dict) or not manifest: + raise ValueError("baseline_files must retain the reviewed path, mode and blob identities") + return baseline, patterns, manifest + + +def policy_patterns(policy: dict) -> tuple[str, ...]: + patterns = policy.get("legacy_paths") + if not isinstance(patterns, list) or not patterns or not all(isinstance(item, str) and item for item in patterns): + raise ValueError("legacy_paths must classify the frozen capsule surfaces") + return tuple(patterns) + + +def working_entry(root: Path, name: str, *, filemode: bool, index_mode: str | None) -> list[str] | None: + path = root / name + try: + mode = path.lstat().st_mode + except FileNotFoundError: + return None + if stat.S_ISLNK(mode): + kind, data = "120000", os.fsencode(os.readlink(path)) + filters = "--no-filters" + elif stat.S_ISREG(mode): + kind = "100755" if filemode and mode & stat.S_IXUSR else "100644" + if not filemode and index_mode in {"100644", "100755"}: + kind = index_mode + data = path.read_bytes() + filters = f"--path={name}" + else: + return ["unsupported-type", ""] + blob = ( + subprocess.run( + ["git", "-C", str(root), "hash-object", filters, "--stdin"], input=data, capture_output=True, check=True + ) + .stdout.decode() + .strip() + ) + return [kind, blob] + + +def manifest_changes(root: Path, manifest: dict[str, list[str]], patterns: tuple[str, ...]) -> list[tuple[str, str]]: + listed = ( + subprocess.check_output( + ["git", "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard", "-z"] + ) + .decode() + .split("\0") + ) + mode = subprocess.check_output(["git", "-C", str(root), "config", "--get", "core.filemode"], text=True).strip() + modes = index_modes(root) + changes = [] + for name in sorted(set(listed) | set(manifest)): + if not name or not any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns): + continue + observed = working_entry(root, name, filemode=mode != "false", index_mode=modes.get(name)) + expected = manifest.get(name) + if observed is None: + continue + if observed != expected: + changes.append(("M" if expected else "A", name)) + return changes + + +def index_modes(root: Path) -> dict[str, str]: + records = subprocess.check_output(["git", "-C", str(root), "ls-files", "--stage", "-z"]).decode().split("\0") + return {name: metadata.split()[0] for record in records if record for metadata, name in [record.split("\t", 1)]} + + +def resolve_commit(root: Path, revision: str) -> str: + try: + return subprocess.check_output( + ["git", "-C", str(root), "rev-parse", "--verify", "--end-of-options", f"{revision}^{{commit}}"], + stderr=subprocess.PIPE, + text=True, + ).strip() + except subprocess.CalledProcessError as exc: + raise ValueError(f"freeze_ref comparison unavailable: {revision}; fetch the recorded Git history") from exc + + +def changed_paths(root: Path, baseline: str, head: str | None) -> list[tuple[str, str]]: + baseline = resolve_commit(root, baseline) + head = resolve_commit(root, head) if head is not None else None + command = ["git", "-C", str(root), "diff", "--name-status", "-z", "--no-renames", baseline] + if head: + command.append(head) + try: + records = subprocess.check_output([*command, "--"], stderr=subprocess.PIPE).decode().split("\0") + except subprocess.CalledProcessError as exc: + raise ValueError(f"freeze_ref comparison unavailable: {baseline}; fetch the recorded Git history") from exc + changes = list(zip(records[0:-1:2], records[1:-1:2], strict=True)) + if head is None: + untracked = subprocess.check_output( + ["git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "-z"], stderr=subprocess.PIPE + ).decode() + changes.extend(("A", name) for name in untracked.split("\0") if name) + return changes + + +def rejected_paths(changes: list[tuple[str, str]], patterns: tuple[str, ...]) -> list[str]: + return sorted( + {path for status, path in changes if status != "D" and any(fnmatch.fnmatchcase(path, p) for p in patterns)} + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Reject renewed capsule work during the native review migration.") + parser.add_argument("--base-ref", help="Read-only historical comparison baseline") + parser.add_argument("--head-ref", help="Read-only historical comparison head; default is the working tree") + arguments = parser.parse_args() + try: + baseline, patterns, manifest = read_policy(ROOT) + changes = ( + changed_paths(ROOT, arguments.base_ref or baseline, arguments.head_ref) + if arguments.base_ref or arguments.head_ref + else manifest_changes(ROOT, manifest, patterns) + ) + except (OSError, ValueError, subprocess.CalledProcessError) as exc: + print(f"Native review scope check failed: {exc}") + return 1 + rejected = rejected_paths(changes, patterns) + if rejected: + for path in rejected: + print(f"{path}: capsule additions/repairs are frozen; retirement deletions remain allowed.") + print( + "Use native execution in packages/specfact-codebase/src/specfact_codebase/project_runtime.py. " + "Follow changes 17 -> 18 -> 19 -> paired core adoption -> 20 in openspec/CHANGE_ORDER.md; " + "do not reopen capsule implementation." + ) + return 1 + print("Native review scope check passed; existing gate outcomes remain authoritative.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/native_pr_review.py b/scripts/native_pr_review.py new file mode 100644 index 00000000..8de64a77 --- /dev/null +++ b/scripts/native_pr_review.py @@ -0,0 +1,389 @@ +"""CI-only completeness barrier around the independently installed signed reviewer.""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib +import importlib.metadata +import json +import os +import re +import stat +import subprocess +import sys +import tomllib +import xml.etree.ElementTree as ET +from collections.abc import Callable +from functools import partial +from pathlib import Path +from typing import Any + +import yaml + + +REQUIRED_MEMBERS = ( + "ruff", + "radon", + "semgrep-clean", + "semgrep-bugs", + "ai-bloat-ast", + "ast-clean-code", + "basedpyright", + "pylint", + "contracts", + "targeted-pytest-coverage", +) +_INCOMPLETE_STATES = {"UNKNOWN", "skipped", "error", "timeout", "unavailable"} +_TEST_CORRESPONDENCE = { + "tools/smart_test_coverage.py": ( + "tests/unit/test_capsule_proof_contexts.py", + "tests/unit/specfact_code_review/run/test_reviewer_bootstrap.py", + ), +} + + +def _incomplete(finding: dict) -> bool: + return ( + finding.get("category") == "tool_error" + or finding.get("evidence_outcome") in _INCOMPLETE_STATES + or finding.get("execution_state") in _INCOMPLETE_STATES + ) + + +def _validate_receipt(receipt: dict, *, expected_head: str) -> None: + members = receipt.get("members") + if not isinstance(members, list) or any(not isinstance(item, dict) for item in members): + raise ValueError("incomplete member inventory") + ids = [item.get("id") for item in members] + if len(ids) != len(REQUIRED_MEMBERS) or set(ids) != set(REQUIRED_MEMBERS): + raise ValueError("incomplete or duplicate member inventory") + for item in members: + _validate_member(item, no_python_sources=receipt.get("files") == {}) + if receipt.get("head") != expected_head: + raise ValueError("receipt head differs from event head") + if not receipt.get("source_before") or receipt.get("source_before") != receipt.get("source_after"): + raise ValueError("source changed during review") + if receipt.get("policy_exit") != 0: + raise ValueError("native review policy failed") + + +def _member_not_applicable(item: dict, no_python_sources: bool) -> bool: + return ( + no_python_sources + and item.get("outcome") == "NOT_APPLICABLE" + and item.get("id") != "targeted-pytest-coverage" + and item.get("reason") == "no_changed_python_sources" + and item.get("findings") == [] + ) + + +def _validate_member(item: dict, *, no_python_sources: bool) -> None: + if _member_not_applicable(item, no_python_sources): + return + findings = item.get("findings") + if item.get("outcome") != "COMPLETE" or not isinstance(findings, list): + raise ValueError("incomplete member execution") + if any(not isinstance(finding, dict) or _incomplete(finding) for finding in findings): + raise ValueError("incomplete raw analyzer evidence") + + +def _rule_identities(root: Path) -> dict[str, str]: + identities = {} + for name in (".semgrep/clean_code.yaml", ".semgrep/bugs.yaml", "resources/semgrep-rules/ai-bloat.yaml"): + path = root / name + if not path.is_file(): + raise ValueError(f"required signed rule pack missing: {name}") + payload = path.read_bytes() + rules = yaml.safe_load(payload).get("rules") + if not isinstance(rules, list) or not rules: + raise ValueError(f"required signed rule pack empty: {name}") + identities[name] = hashlib.sha256(payload).hexdigest() + return identities + + +def _git(root: Path, *args: str) -> str: + return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip() + + +def _source_identity(root: Path) -> str: + digest = hashlib.sha256() + names = subprocess.check_output(["git", "-C", str(root), "ls-files", "-z"]).decode().split("\0") + for name in sorted(set(names) - {""}): + path = root / name + mode = path.lstat().st_mode + if stat.S_ISLNK(mode): + data = os.fsencode(os.readlink(path)) + elif stat.S_ISREG(mode): + data = path.read_bytes() + else: + raise ValueError(f"unsupported tracked input: {name}") + digest.update(json.dumps([name, mode, hashlib.sha256(data).hexdigest()]).encode()) + return digest.hexdigest() + + +def _subject(root: Path, head: str, base: str) -> tuple[str, list[Path]]: + if any(re.fullmatch(r"[0-9a-f]{40}", value) is None for value in (head, base)): + raise ValueError("exact head and merge-base object IDs required") + tree = _git(root, "rev-parse", f"{head}^{{tree}}") + if _git(root, "rev-parse", "HEAD") != base or _git(root, "write-tree") != tree: + raise ValueError("staged subject does not match the exact event head and merge-base") + if subprocess.run(["git", "-C", str(root), "diff", "--quiet", "--"], check=False).returncode: + raise ValueError("working source differs from staged subject") + names = _git(root, "diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z", "--", "*.py") + files = [Path(name) for name in names.split("\0") if name] + for path in files: + if path.is_symlink() or not path.resolve().is_relative_to(root) or not path.is_file(): + raise ValueError(f"unsafe or missing review input: {path}") + return _source_identity(root), files + + +def _trusted_configuration(root: Path, base: str, output: Path, files: list[Path]) -> tuple[Path, Path, dict]: + payload = _git(root, "show", f"{base}:pyproject.toml") + config = output / "pyproject.toml" + config.write_text(payload + "\n") + parsed = tomllib.loads(payload) + types = dict(parsed["tool"]["basedpyright"]) + types["extraPaths"] = [str(root / path) for path in types.get("extraPaths", [])] + types["venvPath"] = str(root) + types["include"] = [str(root / path) for path in files] + for environment in types.get("executionEnvironments", []): + environment["root"] = str(root / environment["root"]) + type_config = output / "pyrightconfig.json" + type_config.write_text(json.dumps(types)) + return config, type_config, parsed + + +def _analyzers(reviewer: Path, config: Path, type_config: Path) -> list[tuple[str, Callable[..., list[Any]]]]: + modules = { + name: importlib.import_module(f"specfact_code_review.tools.{name}_runner") + for name in ("ruff", "radon", "semgrep", "ai_bloat", "ast_clean_code", "basedpyright", "pylint", "contract") + } + return [ + ("ruff", partial(modules["ruff"].run_ruff, extra_args=("--config", str(config), "--no-respect-gitignore"))), + ("radon", modules["radon"].run_radon), + ("semgrep-clean", partial(modules["semgrep"].run_semgrep, bundle_root=reviewer)), + ("semgrep-bugs", partial(modules["semgrep"].run_semgrep_bugs, bundle_root=reviewer)), + ("ai-bloat-ast", modules["ai_bloat"].run_ai_bloat), + ("ast-clean-code", modules["ast_clean_code"].run_ast_clean_code), + ("basedpyright", partial(modules["basedpyright"].run_basedpyright, extra_args=("--project", str(type_config)))), + ("pylint", partial(modules["pylint"].run_pylint, extra_args=("--rcfile", str(config)))), + ("contracts", partial(modules["contract"].run_contract_check, bug_hunt=True)), + ] + + +def _native_test_command(*, python: Path, config: Path, output: Path, selectors: tuple[str, ...] = ()) -> list[str]: + required = {"tests/host/proof_capsule_deferred_review_ci.py", "tests/integration/test_hatch_project_runtime.py"} + command = [str(python), "-m", "pytest", *sorted(required | set(selectors))] + command.extend( + ( + "-c", + str(config), + "--rootdir=.", + "-o", + "junit_family=xunit1", + f"--cov-config={config}", + f"--junitxml={output / 'tests.xml'}", + "--cov=.", + "--cov-fail-under=0", + f"--cov-report=json:{output / 'coverage.json'}", + ) + ) + return command + + +def _source_tests(root: Path, relative: Path) -> tuple[Path, ...]: + mapped = _TEST_CORRESPONDENCE.get(relative.as_posix()) + if mapped is not None: + if any(not (root / path).is_file() for path in mapped): + raise ValueError(f"required mapped tests missing: {relative}") + return tuple(Path(path) for path in mapped) + expected = Path("tests/unit") / relative.parent / f"test_{relative.name}" + if (root / expected).is_file(): + return (expected,) + unit = root / "tests/unit" + flat = [unit / f"test_{relative.name}", *sorted(unit.glob(f"test_{relative.stem}_*.py"))] + existing = tuple(path.relative_to(root) for path in flat if path.is_file()) + if existing: + return existing + package = root / "tests/unit" / relative.parts[0] + return tuple(path.relative_to(root) for path in sorted(package.rglob("test_*.py"))) + + +def _test_inventory(root: Path, files: list[Path], *, roots: tuple[str, ...]) -> tuple[str, ...]: + tests = {str(path) for path in files if path.parts[0] == "tests" and path.name.startswith("test_")} + for path in files: + if path.parts[0] not in roots or "tests" in path.parts: + continue + relative = Path(*path.parts[path.parts.index("src") + 1 :]) if "src" in path.parts else path + candidates = _source_tests(root, relative) + if not candidates: + raise ValueError(f"required package tests missing: {relative.parts[0]}") + tests.update(str(path) for path in candidates) + return tuple(sorted(tests)) + + +def _native_test_environment() -> dict[str, str]: + environment = os.environ.copy() + environment.pop("SPECFACT_CODE_REVIEW_CHANGED_DIFF", None) + return environment + + +def _require_native_test_evidence(junit: Path, *, selectors: tuple[str, ...] = ()) -> None: + cases = list(ET.parse(junit).getroot().iter("testcase")) + if not cases or any(case.find(tag) is not None for case in cases for tag in ("failure", "error")): + raise ValueError("native test evidence missing or failed") + if any(case.find("skipped") is not None for case in cases): + raise ValueError("required targeted native test skipped") + required = {"tests/host/proof_capsule_deferred_review_ci.py", "tests/integration/test_hatch_project_runtime.py"} + observed = {case.get("file") for case in cases} + missing = (required | set(selectors)) - observed + if missing: + raise ValueError(f"required native test files missing: {sorted(missing)}") + + +def _native_tests(root: Path, python: Path, output: Path, parsed: dict) -> tuple[list, int]: + from specfact_code_review.run.runner import _coverage_findings + + junit, coverage = output / "tests.xml", output / "coverage.json" + _, files = _subject(root, os.environ["NATIVE_REVIEW_HEAD"], os.environ["NATIVE_REVIEW_BASE"]) + roots = tuple(parsed["tool"]["coverage"]["run"]["source"]) + selectors = _test_inventory(root, files, roots=roots) + command = _native_test_command(python=python, config=output / "pyproject.toml", output=output, selectors=selectors) + command.extend( + ( + "-o", + "pythonpath=" + + " ".join(str(root / path) for path in parsed["tool"]["pytest"]["ini_options"]["pythonpath"]), + ) + ) + with (output / "pytest.log").open("w") as log: + result = subprocess.run( + command, stdout=log, stderr=subprocess.STDOUT, check=False, timeout=600, env=_native_test_environment() + ) + _require_native_test_evidence(junit, selectors=selectors) + sources = [path for path in files if path.parts[0] in roots and "tests" not in path.parts] + findings, _ = _coverage_findings( + sources, + json.loads(coverage.read_text()), + threshold=float(parsed["tool"]["coverage"]["report"]["fail_under"]), + blocking_low_coverage=True, + ) + return findings, result.returncode + + +def _run_analyzer(run: Callable[..., list[Any]], files: list[Path], record: dict) -> list[Any]: + records = run(files) + record.update(outcome="COMPLETE", findings=[f.model_dump(mode="json") for f in records]) + if any(_incomplete(finding) for finding in record["findings"]): + record["outcome"] = "INCOMPLETE" + raise ValueError(f"required analyzer incomplete: {record['id']}") + return records + + +def _tool_versions() -> dict[str, str]: + versions = { + name: importlib.metadata.version(name) + for name in ("specfact-cli", "ruff", "radon", "basedpyright", "pylint", "crosshair-tool") + } + try: + result = subprocess.run(["semgrep", "--version"], capture_output=True, text=True, check=True, timeout=30) + except subprocess.CalledProcessError as error: + raise ValueError(f"Semgrep startup failed: {(error.stderr or '')[-2000:]}") from error + versions["semgrep"] = result.stdout.strip() + if not versions["semgrep"]: + raise ValueError("required Semgrep version is unavailable") + return versions + + +def _review(root: Path, reviewer: Path, project_python: Path, output: Path, receipt: dict) -> None: + from specfact_code_review.run.findings import ReviewReport + from specfact_code_review.run.runner import _suppress_known_noise, _with_enforcement + from specfact_code_review.run.scorer import score_review + + head, base = receipt["head"], receipt["merge_base"] + before, files = _subject(root, head, base) + config, type_config, parsed = _trusted_configuration(root, base, output, files) + receipt.update( + { + "source_before": before, + "rules": _rule_identities(reviewer), + "configuration": {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in (config, type_config)}, + "files": {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in files}, + "tools": _tool_versions(), + } + ) + findings = [] + for member, run in _analyzers(reviewer, config, type_config): + if not files: + receipt["members"].append( + {"id": member, "outcome": "NOT_APPLICABLE", "reason": "no_changed_python_sources", "findings": []} + ) + continue + record = {"id": member, "outcome": "INCOMPLETE", "findings": []} + receipt["members"].append(record) + findings.extend(_run_analyzer(run, files, record)) + record = {"id": "targeted-pytest-coverage", "outcome": "INCOMPLETE", "findings": []} + receipt["members"].append(record) + tests, test_exit = _native_tests(root, project_python, output, parsed) + findings.extend(tests) + record.update(outcome="COMPLETE", findings=[f.model_dump(mode="json") for f in tests]) + findings = _suppress_known_noise(findings) + score = score_review(findings) + report = ReviewReport( + run_id=f"native-ci-{head}", score=score.score, findings=findings, summary="Independent native CI review" + ) + report = _with_enforcement(report, mode="changed", files=files) + receipt["policy_exit"] = int(bool(report.ci_exit_code or test_exit or any(f.is_blocking() for f in tests))) + receipt["policy_report"] = report.model_dump(mode="json") + receipt["source_after"], _ = _subject(root, head, base) + + +def _reviewer_identity(reviewer: Path) -> dict[str, str]: + manifest = reviewer.parent.parent / "module-package.yaml" + data = manifest.read_bytes() + if yaml.safe_load(data).get("version") != "0.51.2": + raise ValueError("independent reviewer version differs from verified installed version") + for name, module in tuple(sys.modules.items()): + if name == "specfact_code_review" or name.startswith("specfact_code_review."): + source = getattr(module, "__file__", None) + if source is None or not Path(source).resolve().is_relative_to(reviewer): + raise ValueError("reviewer import resolved outside signed installation") + return {"version": "0.51.2", "manifest_sha256": hashlib.sha256(data).hexdigest()} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--subject", type=Path, required=True) + parser.add_argument("--project-python", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + from specfact_cli.registry import CommandRegistry + + CommandRegistry.get_module_typer("code") + import specfact_code_review + + reviewer = Path(specfact_code_review.__file__).resolve().parent + if reviewer.is_relative_to(args.subject.resolve()): + raise ValueError("independent reviewer resolved inside candidate") + args.output.mkdir(parents=True, exist_ok=True) + os.chdir(args.subject) + head, base = os.environ["NATIVE_REVIEW_HEAD"], os.environ["NATIVE_REVIEW_BASE"] + os.environ["SPECFACT_CODE_REVIEW_CHANGED_DIFF"] = "cached" + receipt = {"head": head, "merge_base": base, "members": [], "status": "INCOMPLETE"} + try: + receipt["reviewer"] = _reviewer_identity(reviewer) + _review(args.subject.resolve(), reviewer, args.project_python, args.output, receipt) + _reviewer_identity(reviewer) + _validate_receipt(receipt, expected_head=head) + receipt["status"] = "PASS" + except Exception as exc: + receipt["diagnostic"] = f"{type(exc).__name__}: {str(exc)[:1000]}" + raise + finally: + (args.output / "receipt.json").write_text(json.dumps(receipt, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/pre-commit-quality-checks.sh b/scripts/pre-commit-quality-checks.sh index 7ba0b590..cf09fccd 100755 --- a/scripts/pre-commit-quality-checks.sh +++ b/scripts/pre-commit-quality-checks.sh @@ -327,16 +327,16 @@ run_code_review_gate() { exit 1 fi local indexed_workflow - if ! indexed_workflow="$(git show :.github/workflows/capsule-customer-execution.yml 2>/dev/null)"; then - error "Capsule review deferral requires the indexed blocking GitHub Linux workflow." + if ! indexed_workflow="$(git show :.github/workflows/native-pr-review.yml 2>/dev/null)"; then + error "Capsule review deferral requires the indexed blocking GitHub Linux native workflow." exit 1 fi - if ! grep -Fq 'name: Run deferred candidate commit review without weakening enforcement' <<<"${indexed_workflow}"; then - error "Capsule review deferral requires the blocking GitHub Linux candidate review step." + if ! grep -Fq 'name: independent signed native review' <<<"${indexed_workflow}"; then + error "Capsule review deferral requires the blocking independent signed-review job (native)." exit 1 fi - if ! grep -Fq 'name: independent signed capsule review' <<<"${indexed_workflow}"; then - error "Capsule review deferral requires the blocking independent signed-review job." + if ! grep -Fq 'name: Run complete independent native review' <<<"${indexed_workflow}"; then + error "Capsule review deferral requires the blocking GitHub Linux native review step." exit 1 fi local capsule_paths candidate_base @@ -345,7 +345,7 @@ run_code_review_gate() { exit 1 fi if ! capsule_paths="$(git diff --cached --name-only "${candidate_base}" -- \ - packages/specfact-code-review .github/workflows/capsule-customer-execution.yml)"; then + packages/specfact-code-review tests/fixtures/portable-runtime .github/workflows/capsule-customer-execution.yml)"; then error "Capsule review deferral cannot determine the staged candidate delta against origin/dev." exit 1 fi @@ -353,7 +353,7 @@ run_code_review_gate() { error "Capsule review deferral requires a candidate change that schedules the blocking capsule workflow." exit 1 fi - warn "DEFERRED: only the local capsule review requires mandatory exact-head GitHub Linux CI; this is not a PASS." + warn "DEFERRED: only the local capsule review requires mandatory exact-head signed native GitHub Linux review; this is not a PASS." return 0 fi local review_array=() diff --git a/scripts/prove_hatch_project_runtime.py b/scripts/prove_hatch_project_runtime.py new file mode 100644 index 00000000..080e098b --- /dev/null +++ b/scripts/prove_hatch_project_runtime.py @@ -0,0 +1,214 @@ +"""Repeatable first-slice native Hatch proof. Receipt and projects stay external.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import subprocess +import sys +from dataclasses import asdict +from pathlib import Path + +from specfact_codebase.project_runtime import ( + PreparedProject, + ProjectCommand, + ProjectSelection, + inspect_project, + prepare_project, +) + + +ROOT = Path(__file__).resolve().parents[1] +COMPANION = ROOT / "tests/fixtures/portable-runtime/hatch-bootstrap-companion" +ORIGINAL = ROOT / "tests/fixtures/portable-runtime/hatch-detached" +INVENTORY = """import importlib.metadata as m,json +print(json.dumps(sorted((d.metadata['Name'],d.version,d.read_text('direct_url.json')) for d in m.distributions()))) +""" + + +def fixture_files(root: Path): + return sorted( + path + for path in root.rglob("*") + if path.is_file() + and not {".ruff_cache", ".pytest_cache", "__pycache__"}.intersection(path.relative_to(root).parts) + ) + + +def files_identity(root: Path) -> dict[str, str]: + return {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() for p in fixture_files(root)} + + +def declarations(root: Path) -> dict[str, str]: + return { + name: hashlib.sha256((root / name).read_bytes()).hexdigest() + for name in ("pyproject.toml", "hatch.toml", ".python-version") + } + + +def materialize(root: Path) -> None: + for source in fixture_files(COMPANION): + relative = source.relative_to(COMPANION) + target = root / relative.with_suffix("") if source.suffix == ".in" else root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(source.read_bytes()) + + +def inventory(python: Path, root: Path) -> list: + process = subprocess.run( + [str(python), "-I", "-c", INVENTORY], cwd=root, capture_output=True, text=True, check=True, timeout=30 + ) + return json.loads(process.stdout) + + +def _prepare_existing(root: Path, workspace: Path, hatch: Path, python: Path): + prefix = workspace / "existing-native" + config = root / "hatch.toml" + config.write_text(config.read_text() + f"path = {json.dumps(str(prefix))}\n") + environment = { + key: value + for key, value in os.environ.items() + if key not in {"PYTHONPATH", "PYTHONHOME", "VIRTUAL_ENV", "__PYVENV_LAUNCHER__"} + and not key.startswith("HATCH_") + } + environment["HATCH_PYTHON"] = str(python) + process = subprocess.run( + [str(hatch), "env", "create", "review"], + cwd=root, + env=environment, + capture_output=True, + text=True, + timeout=180, + check=False, + ) + setup = {"exit_code": process.returncode, "stdout": process.stdout, "stderr": process.stderr} + assert process.returncode == 0, setup + selected_python = prefix / "bin/python" + return selected_python, setup, inventory(selected_python, root) + + +def _run_command(prepared: PreparedProject, root: Path, argv): + return prepared.run(ProjectCommand(tuple(argv), root), timeout=60) + + +def _record_command(commands: dict, name: str, result, expected: int): + commands[name] = asdict(result) + assert result.outcome == "exited" and result.exit_code == expected, (name, result) + + +def _run_checks(prepared: PreparedProject, root: Path) -> dict: + commands = {} + imports = _run_command( + prepared, + root, + [ + "python", + "-c", + "import requests,pytest,pytest_asyncio,sys; print(sys.executable); " + "print(sys.prefix); print(requests.Request('GET','https://example.invalid').prepare().method)", + ], + ) + _record_command(commands, "imports", imports, 0) + entrypoint = _run_command(prepared, root, ["pytest", "--version"]) + _record_command(commands, "dependency_entrypoint", entrypoint, 0) + tests = _run_command(prepared, root, ["python", "-m", "pytest", "-p", "no:asyncio", "-v"]) + _record_command(commands, "native_async_pytest", tests, 0) + assert "test_runtime_dependencies PASSED" in tests.stdout and "8 passed" in tests.stdout, tests + lint = _run_command(prepared, root, ["ruff", "check", "lint_probe.py"]) + _record_command(commands, "enabled_customer_rule", lint, 1) + assert "F821" in lint.stdout and "F401" not in lint.stdout + disabled = _run_command(prepared, root, ["ruff", "check", "lint_probe.py", "--ignore", "F821"]) + _record_command(commands, "disabled_customer_rule", disabled, 0) + seeded = root / "tests/test_seeded_failure.py" + seeded.write_text("def test_seeded_customer_defect():\n assert 2 + 2 == 5\n") + defect = _run_command(prepared, root, ["python", "-m", "pytest", "-p", "no:asyncio", "-v"]) + _record_command(commands, "seeded_real_failure", defect, 1) + assert "test_seeded_customer_defect FAILED" in defect.stdout and "1 failed" in defect.stdout + assert defect.source_identity != tests.source_identity + seeded.unlink() + return commands + + +def _prove_case(workspace: Path, case: str, hatch: Path, python: Path) -> dict: + root = workspace / case / "customer" + materialize(root) + setup = None + if case == "existing": + selected_python, setup, before_inventory = _prepare_existing(root, workspace, hatch, python) + else: + selected_python = python + before_inventory = None + before_declarations = declarations(root) + plan = inspect_project( + root, + ProjectSelection( + environment="review", python=str(selected_python) if case == "cold" else None, hatch=str(hatch) + ), + ) + prepared = prepare_project(plan, workspace / case / "invocation") + assert isinstance(prepared, PreparedProject), prepared + assert prepared.identity.ownership == ("existing" if case == "existing" else "invocation") + assert prepared.identity.interpreter.version == (3, 11, 15) + actual_python = prepared.identity.interpreter.executable + installed_before_checks = inventory(actual_python, root) + assert not any( + name.lower().startswith("specfact") or name.lower() in {"crosshair-tool", "beartype", "icontract"} + for name, _, _ in installed_before_checks + ) + commands = _run_checks(prepared, root) + after_inventory = inventory(actual_python, root) + assert after_inventory == installed_before_checks + if before_inventory is not None: + assert after_inventory == before_inventory + assert declarations(root) == before_declarations + return { + "identity": asdict(prepared.identity), + "setup": setup, + "declarations_before": before_declarations, + "declarations_after": declarations(root), + "inventory_before": before_inventory or installed_before_checks, + "inventory_after": after_inventory, + "commands": commands, + } + + +def prove(workspace: Path, *, hatch: Path, python: Path) -> dict: + if not __debug__: + raise RuntimeError("Native proof refuses optimized Python; assertions must remain enabled") + if sys.version_info[:2] != (3, 12): + raise RuntimeError("Proof requires controller Python 3.12") + workspace = workspace.resolve() + if workspace.exists() or workspace.is_relative_to(ROOT): + raise ValueError("Use a fresh external proof workspace") + workspace.mkdir(parents=True, mode=0o700) + original = files_identity(ORIGINAL) + receipt = { + "scope": "first existing+cold detached Hatch milestone only", + "explicit_pytest_args": ["-p", "no:asyncio"], + "companion_delta": "pyodbc/native assertions omitted only in companion; customer Ruff/config added", + "controller": {"executable": sys.executable, "version": list(sys.version_info[:3])}, + "hatch": str(hatch), + "cases": {}, + } + for case in ("existing", "cold"): + receipt["cases"][case] = _prove_case(workspace, case, hatch, python) + assert files_identity(ORIGINAL) == original + receipt["original_fixture_unchanged"] = original + (workspace / "receipt.json").write_text(json.dumps(receipt, indent=2, default=str)) + return receipt + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--workspace", required=True, type=Path) + parser.add_argument("--hatch", required=True, type=Path) + parser.add_argument("--python", required=True, type=Path) + arguments = parser.parse_args() + prove(arguments.workspace, hatch=arguments.hatch, python=arguments.python) + sys.stdout.write(f"{arguments.workspace / 'receipt.json'}\n") + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/native-review-scope-history.json b/tests/fixtures/native-review-scope-history.json new file mode 100644 index 00000000..e33c9965 --- /dev/null +++ b/tests/fixtures/native-review-scope-history.json @@ -0,0 +1,8 @@ +{ + "baseline_revision": "59611f358572aaa3c383c2d2fffcf82fb57ff36c", + "repair_revision": "e2b6487679e7ecd06cb8dea7537a473584990f93", + "path": ".github/workflows/capsule-customer-execution.yml", + "scope": "Exact inert excerpt of the original diagnostic repair; not a retained full historical checkout.", + "before": " detail = test_failure_details.get((nodeid, outcome, phase), \"\")\n exception = re.search(r\"^E\\s+(?:subprocess\\.)?(AssertionError|FileNotFoundError|PermissionError|CalledProcessError|RuntimeError|ImportError|ModuleNotFoundError|TypeError|ValueError)(?=:|\\s|$)\", detail, re.MULTILINE)\n failure = {\"test_failure_class\": exception.group(1)} if exception else {}\n for option in (\"-I\", \"-E\", \"-S\"):\n", + "after": " detail = test_failure_details.get((nodeid, outcome, phase), \"\")\n exception = re.search(r\"^E\\s+(?:subprocess\\.)?(AssertionError|FileNotFoundError|PermissionError|CalledProcessError|RuntimeError|ImportError|ModuleNotFoundError|TypeError|ValueError)(?=:|\\s|$)\", detail, re.MULTILINE)\n failure = {\"test_failure_class\": exception.group(1)} if exception else {}\n native_skips = {\n \"Set SPECFACT_HATCH_PROOF_WORKSPACE with native Hatch and Python 3.11 prerequisites for the acceptance cell\",\n \"Native Hatch and project Python are required for customer storage acceptance\",\n \"Native Hatch and project Python are required for absolute declaration acceptance\",\n }\n if outcome == \"skipped\" and detail in native_skips:\n failure[\"test_diagnostic\"] = \"native_prerequisites_not_supplied\"\n for option in (\"-I\", \"-E\", \"-S\"):\n" +} diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/.python-version b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/.python-version new file mode 100644 index 00000000..ed7d51a3 --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/.python-version @@ -0,0 +1 @@ +3.11.15 diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/README.md b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/README.md new file mode 100644 index 00000000..f6d4573f --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/README.md @@ -0,0 +1,3 @@ +# First Hatch bootstrap companion + +This explicitly adapted companion preserves the original detached review versus default pytest conflict, src/tools import paths and native pytest-asyncio execution. It omits pyodbc and its native-library assertions only here, pending later native dependency acceptance. It declares customer Ruff and ignores F401 while enabling F821. The original hatch-detached reconstruction remains unchanged. diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/hatch.toml b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/hatch.toml new file mode 100644 index 00000000..3d24faf5 --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/hatch.toml @@ -0,0 +1,9 @@ +[envs.default] +dependencies = ["pytest<8"] + +[envs.review] +detached = true +dependencies = ["pytest>=9,<10", "pytest-asyncio>=1,<2", "requests>=2.32,<3", "ruff>=0.14,<1"] + +python = "3.11" +python-sources = ["external"] diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/lint_probe.py.in b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/lint_probe.py.in new file mode 100644 index 00000000..9673111d --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/lint_probe.py.in @@ -0,0 +1,3 @@ +import os + +undefined_customer_name diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/pyproject.toml b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/pyproject.toml new file mode 100644 index 00000000..c02c8d80 --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "specfact-472-reconstructed-fixture" +version = "0.0.0" +requires-python = ">=3.11,<3.14" + +[tool.pytest.ini_options] +addopts = "-ra -v --import-mode=importlib -p pytest_asyncio.plugin" +pythonpath = ["src", "tools", "."] +testpaths = ["tests"] +asyncio_mode = "auto" + +[tool.ruff.lint] +select = ["F"] +ignore = ["F401"] diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/src/portable_customer/__init__.py.in b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/src/portable_customer/__init__.py.in new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/src/portable_customer/core.py.in b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/src/portable_customer/core.py.in new file mode 100644 index 00000000..dc33fb4a --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/src/portable_customer/core.py.in @@ -0,0 +1,5 @@ +import requests + + +def prepared_method() -> str: + return requests.Request("GET", "https://example.invalid").prepare().method diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/tests/test_core.py.in b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/tests/test_core.py.in new file mode 100644 index 00000000..4db4275c --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/tests/test_core.py.in @@ -0,0 +1,140 @@ +import json +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +from fixture_value import VALUE +from portable_customer.core import prepared_method + + +async def test_runtime_dependencies() -> None: + assert prepared_method() == "GET" + assert VALUE == 42 + + +def test_nested_python_inherits_environment(monkeypatch) -> None: + monkeypatch.setenv("PORTABLE_INHERITED", "parent-added") + result = subprocess.run( + [sys.executable, "-c", "import os; print(os.environ['PORTABLE_INHERITED'])"], + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert result.stdout.strip() == "parent-added" + + +def test_nested_python_honors_explicit_environment(monkeypatch) -> None: + monkeypatch.setenv("PORTABLE_OVERRIDE", "parent") + monkeypatch.setenv("PORTABLE_REMOVED", "parent-only") + environment = dict(os.environ) + environment.update(PORTABLE_ADDED="child-added", PORTABLE_OVERRIDE="child", HOME="/portable-child-home") + del environment["PORTABLE_REMOVED"] + script = ( + "import json,os; print(json.dumps([os.getenv(name) for name in " + "('PORTABLE_ADDED','PORTABLE_OVERRIDE','PORTABLE_REMOVED','HOME')]))" + ) + result = subprocess.run( + [sys.executable, "-c", script], + env=environment, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == ["child-added", "child", None, "/portable-child-home"] + assert os.environ["PORTABLE_OVERRIDE"] == "parent" + assert os.environ["PORTABLE_REMOVED"] == "parent-only" + + +def test_nested_python_accepts_minimal_environment(monkeypatch) -> None: + monkeypatch.setenv("PORTABLE_REMOVED", "parent-only") + result = subprocess.run( + [ + sys.executable, + "-c", + "import json,os; print(json.dumps([os.getenv('PORTABLE_ADDED'),os.getenv('PORTABLE_REMOVED'),os.getenv('HOME')]))", + ], + env={"PORTABLE_ADDED": "minimal"}, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == ["minimal", None, None] + + +def test_nested_python_empty_environment_keeps_attached_imports() -> None: + source = Path(__file__).resolve().parents[1] / "src" + script = ( + "import json,sys; sys.path.insert(0, sys.argv[1]); " + "from portable_customer.core import prepared_method; " + "print(json.dumps([prepared_method(), isinstance([], list)]))" + ) + result = subprocess.run( + [sys.executable, "-c", script, str(source)], + env={}, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == ["GET", True] + + +def test_nested_python_preserves_cwd_and_relative_files(tmp_path: Path) -> None: + (tmp_path / "payload.txt").write_text("caller-data", encoding="utf-8") + (tmp_path / "helper.py").write_text( + "import json\nfrom pathlib import Path\n" + "print(json.dumps([str(Path.cwd()), Path('payload.txt').read_text(encoding='utf-8')]))\n", + encoding="utf-8", + ) + result = subprocess.run( + [sys.executable, "helper.py"], + cwd=tmp_path, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == [str(tmp_path.resolve()), "caller-data"] + + +def test_nested_python_shares_caller_owned_temporary_file() -> None: + for directory in (None, "/tmp"): + with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", dir=directory) as stream: + stream.write("parent-data") + stream.flush() + script = ( + "import sys; from pathlib import Path; path=Path(sys.argv[1]); " + "assert path.read_text(encoding='utf-8') == 'parent-data'; " + "path.write_text('child-data', encoding='utf-8')" + ) + subprocess.run([sys.executable, "-c", script, stream.name], check=True, timeout=30) + stream.seek(0) + assert stream.read() == "child-data" + + +def test_nested_python_preserves_explicit_pythonpath(tmp_path: Path) -> None: + extra = tmp_path / "extra" + extra.mkdir() + (extra / "caller_only.py").write_text("VALUE = 'caller-path'\n", encoding="utf-8") + child = "import caller_only; print(caller_only.VALUE)" + script = ( + "import caller_only,os,subprocess,sys; " + "assert caller_only.VALUE == 'caller-path'; " + "assert os.environ['PYTHONPATH'] == 'extra'; " + "subprocess.run([sys.executable, '-c', sys.argv[1]], check=True, timeout=30)" + ) + result = subprocess.run( + [sys.executable, "-c", script, child], + cwd=tmp_path, + env={"PYTHONPATH": "extra"}, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert result.stdout.strip() == "caller-path" diff --git a/tests/fixtures/portable-runtime/hatch-bootstrap-companion/tools/fixture_value.py.in b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/tools/fixture_value.py.in new file mode 100644 index 00000000..b11ef08a --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-bootstrap-companion/tools/fixture_value.py.in @@ -0,0 +1 @@ +VALUE = 42 diff --git a/tests/host/proof_capsule_deferred_review_ci.py b/tests/host/proof_capsule_deferred_review_ci.py index bf55633a..02f12671 100644 --- a/tests/host/proof_capsule_deferred_review_ci.py +++ b/tests/host/proof_capsule_deferred_review_ci.py @@ -3,12 +3,14 @@ import os import subprocess import sys +import textwrap import venv from pathlib import Path import pytest import yaml +from specfact_code_review.run.runtime_discovery import discover_project from tests.support.capsule_review_fixtures import ( _BLOCK2_HATCH_FIXTURE, _TRUSTED_BOOTSTRAP_LAUNCHER, @@ -21,9 +23,15 @@ create_isolated_reviewer, deferral_worktree, deferred_review_repository, + independent_review_job, ) +@pytest.fixture(autouse=True) +def acceptance_selector(request, record_property): + record_property("specfact.selector", request.node.nodeid) + + @pytest.mark.parametrize("gate_exit", [0, 7]) @pytest.mark.parametrize("advanced_dev", [False, True]) def test_deferred_gate_reviews_exact_staged_tree_and_propagates_failure( @@ -55,8 +63,6 @@ def test_deferred_gate_reviews_exact_staged_tree_and_propagates_failure( ["bash", "-c", step["run"]], cwd=repository, env=environment, capture_output=True, text=True, check=False ) assert result.returncode == gate_exit, result.stdout + result.stderr - from specfact_code_review.run.runtime_discovery import discover_project - plan = discover_project(REPO_ROOT, config_path=customer / "commit-review-project.toml") assert (plan.manager, plan.environment) == ("hatch", "default") assert not list((customer / "cache").iterdir()) @@ -130,8 +136,6 @@ def test_isolated_reviewer_preloads_trusted_code_and_never_accepts_incomplete_pr def test_trusted_bootstrap_cannot_import_candidate_venv_module(tmp_path: Path) -> None: - import textwrap - workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/capsule-customer-execution.yml").read_text()) step = workflow["jobs"]["independent-review"]["steps"][2] candidate = tmp_path / "candidate" @@ -169,8 +173,6 @@ def test_deferred_host_fixture_does_not_alias_managed_caller_python(tmp_path: Pa @pytest.mark.parametrize("review_exit", [0, 2, 7]) def test_independent_no_impact_still_reviews_and_cleans_resolution(tmp_path: Path, review_exit: int) -> None: - from tests.support.capsule_review_fixtures import independent_review_job - step = next(item for item in independent_review_job()["steps"] if item.get("id") == "independent_review") trusted, environment = create_isolated_reviewer(tmp_path, (review_exit, "NOT_APPLICABLE", "no_governed_impact")) result = subprocess.run(["bash", "-c", step["run"]], env=environment, text=True, capture_output=True, check=False) @@ -193,8 +195,6 @@ def test_independent_no_impact_still_reviews_and_cleans_resolution(tmp_path: Pat def test_independent_invalid_no_impact_stops_before_review( tmp_path: Path, status, reason, selected, scope_exit ) -> None: - from tests.support.capsule_review_fixtures import independent_review_job - step = next(item for item in independent_review_job()["steps"] if item.get("id") == "independent_review") trusted, environment = create_isolated_reviewer( tmp_path, (0, status, reason), selected_paths=selected, scope_exit=scope_exit @@ -205,3 +205,46 @@ def test_independent_invalid_no_impact_stops_before_review( assert not (trusted / "prepared").exists() assert (trusted / "cleaned").exists() assert "private-reason" not in result.stdout + + +@pytest.mark.parametrize( + "staged,independent_review,expected,diagnostic", + [ + (True, True, 0, "DEFERRED: only the local capsule review"), + (False, True, 1, "requires a candidate change that schedules the blocking capsule workflow"), + (True, False, 1, "requires the blocking independent signed-review job"), + ], + ids=["staged-fixture", "unstaged-fixture", "missing-indexed-independent-review"], +) +def test_portable_fixture_deferral_retains_block2_guards( + tmp_path: Path, staged: bool, independent_review: bool, expected: int, diagnostic: str +) -> None: + scenario = ("Darwin", "", "github-linux", "specfact-project", False, independent_review, expected) + worktree = deferral_worktree(tmp_path, scenario) + fixture = worktree / "tests/fixtures/portable-runtime/hatch-bootstrap-companion/pyproject.toml" + fixture.parent.mkdir(parents=True) + fixture.write_text('[project]\nname = "portable-fixture"\n') + if staged: + _git(worktree, "add", str(fixture.relative_to(worktree))) + calls = tmp_path / "calls" + script = (REPO_ROOT / "scripts/pre-commit-quality-checks.sh").read_text().rsplit('main "$@"', 1)[0] + environment = os.environ | { + "FIXTURE_PLATFORM": "Darwin", + "FIXTURE_CALLS": str(calls), + "GITHUB_ACTIONS": "", + "CI": "", + "SPECFACT_CODE_REVIEW_DEFER_TO_CI": "github-linux", + } + result = subprocess.run( + ["bash", "-c", script + _BLOCK2_HATCH_FIXTURE], + cwd=worktree, + env=environment, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == expected, result.stdout + result.stderr + assert diagnostic in result.stderr + assert_block2_trace(calls.read_text(), expected, result.stderr) + if expected == 0: + assert "this is not a PASS" in result.stderr diff --git a/tests/integration/test_hatch_project_runtime.py b/tests/integration/test_hatch_project_runtime.py new file mode 100644 index 00000000..443ebca8 --- /dev/null +++ b/tests/integration/test_hatch_project_runtime.py @@ -0,0 +1,292 @@ +import importlib.util +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_codebase import project_runtime +from specfact_codebase.project_runtime import ( + PreparedProject, + ProjectCommand, + ProjectSelection, + inspect_project, + prepare_project, +) + + +@pytest.fixture(autouse=True) +def record_selector(request, record_property): + record_property("specfact.selector", request.node.nodeid) + + +def test_existing_and_cold_native_hatch_workflow(): + location = os.environ.get("SPECFACT_HATCH_PROOF_WORKSPACE") + if not location: + pytest.skip( + "Set SPECFACT_HATCH_PROOF_WORKSPACE with native Hatch and Python 3.11 prerequisites for the acceptance cell" + ) + prerequisites = ("SPECFACT_HATCH_EXECUTABLE", "SPECFACT_HATCH_PROJECT_PYTHON") + missing = [name for name in prerequisites if not os.environ.get(name)] + if missing: + pytest.fail("Configured native acceptance cell requires: " + ", ".join(missing)) + hatch, python = (os.environ[name] for name in prerequisites) + script = Path(__file__).resolve().parents[2] / "scripts/prove_hatch_project_runtime.py" + spec = importlib.util.spec_from_file_location("hatch_runtime_proof", script) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + receipt = module.prove( + Path(location), + hatch=Path(hatch), + python=Path(python), + ) + assert set(receipt["cases"]) == {"existing", "cold"} + + +def _create_storage_customer(tmp_path, hatch, python): + root = tmp_path / "customer" + root.mkdir() + (root / "pyproject.toml").write_text('[project]\nname="storage-customer"\nversion="0"\n') + (root / "hatch.toml").write_text("[envs.review]\ndetached=true\ndependencies=[]\n") + data = tmp_path / "customer-data" + cache = tmp_path / "customer-cache" + environment = { + key: value + for key, value in os.environ.items() + if not key.startswith("HATCH_") and key not in {"PYTHONPATH", "PYTHONHOME", "VIRTUAL_ENV"} + } + environment.update(HATCH_DATA_DIR=str(data), HATCH_CACHE_DIR=str(cache), HATCH_PYTHON=python) + subprocess.run( + [hatch, "env", "create", "review"], + cwd=root, + env=environment, + capture_output=True, + text=True, + check=True, + timeout=180, + ) + location = subprocess.run( + [hatch, "env", "find", "review"], + cwd=root, + env=environment, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + prefix = Path(location.stdout.strip()) + assert prefix.is_relative_to(data) + return root, data, cache, prefix + + +def test_native_hatch_customer_storage_survives_controller_context(tmp_path, monkeypatch): + hatch = os.environ.get("SPECFACT_HATCH_EXECUTABLE") + python = os.environ.get("SPECFACT_HATCH_PROJECT_PYTHON") + if not hatch or not python: + if os.environ.get("SPECFACT_HATCH_PROOF_WORKSPACE"): + missing = [ + name + for name in ("SPECFACT_HATCH_EXECUTABLE", "SPECFACT_HATCH_PROJECT_PYTHON") + if not os.environ.get(name) + ] + pytest.fail("Configured native acceptance cell requires: " + ", ".join(missing)) + pytest.skip("Native Hatch and project Python are required for customer storage acceptance") + root, data, cache, prefix = _create_storage_customer(tmp_path, hatch, python) + monkeypatch.setenv("HATCH_DATA_DIR", str(data)) + monkeypatch.setenv("HATCH_CACHE_DIR", str(cache)) + monkeypatch.delenv("HATCH_CONFIG", raising=False) + for name in ("HATCH_ENV_ACTIVE", "HATCH_PYTHON", "HATCH_ENV_TYPE_VIRTUAL_PATH", "HATCH_PROJECT"): + monkeypatch.setenv(name, "/controller/invalid") + plan = inspect_project(root, ProjectSelection(environment="review", hatch=hatch)) + prepared = prepare_project(plan, tmp_path / "invocation") + assert isinstance(prepared, PreparedProject), prepared + assert prepared.identity.ownership == "existing" + assert prepared.identity.interpreter.prefix == prefix + result = prepared.run( + ProjectCommand( + ( + "python", + "-c", + "import os,json; print(json.dumps([os.getenv('HATCH_DATA_DIR')," + "os.getenv('HATCH_CACHE_DIR'),os.getenv('HATCH_ENV_ACTIVE')," + "os.getenv('HATCH_PYTHON'),os.getenv('HATCH_ENV_TYPE_VIRTUAL_PATH')," + "os.getenv('HATCH_PROJECT')]))", + ), + root, + ), + timeout=10, + ) + assert result.outcome == "exited" and result.exit_code == 0 + assert json.loads(result.stdout) == [str(data), str(cache), None, None, None, None] + + +@pytest.mark.parametrize("optimization", ["flag", "environment"]) +def test_native_proof_rejects_optimized_python_before_allocation(tmp_path, optimization): + repository = Path(__file__).resolve().parents[2] + workspace = tmp_path / "proof" + command = [sys.executable] + if optimization == "flag": + command.append("-O") + command += [ + str(repository / "scripts/prove_hatch_project_runtime.py"), + "--workspace", + str(workspace), + "--hatch", + str(tmp_path / "missing-hatch"), + "--python", + str(tmp_path / "missing-python"), + ] + environment = os.environ | {"PYTHONPATH": str(repository / "packages/specfact-codebase/src")} + if optimization == "environment": + environment["PYTHONOPTIMIZE"] = "1" + result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=15, check=False) + assert result.returncode != 0 + assert "optimized Python" in result.stderr + assert not workspace.exists() + assert not (workspace / "receipt.json").exists() + + +def test_native_proof_rejects_wrong_controller_before_allocation(tmp_path, monkeypatch): + script = Path(__file__).resolve().parents[2] / "scripts/prove_hatch_project_runtime.py" + spec = importlib.util.spec_from_file_location("hatch_precondition_proof", script) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + monkeypatch.setattr(module, "sys", SimpleNamespace(version_info=(3, 11, 15), executable="wrong-controller")) + workspace = tmp_path / "proof" + with pytest.raises(RuntimeError, match=r"controller Python 3\.12"): + module.prove(workspace, hatch=tmp_path / "missing-hatch", python=tmp_path / "missing-python") + assert not workspace.exists() + assert not (workspace / "receipt.json").exists() + + +def _create_absolute_customer(tmp_path, python, version_file): + expected = json.loads( + subprocess.check_output( + [python, "-I", "-c", "import sys,json; print(json.dumps([sys.base_prefix,list(sys.version_info[:3])]))"], + text=True, + ) + ) + customer = tmp_path / "customer" + customer.mkdir() + (customer / "pyproject.toml").write_text('[project]\nname="absolute-python-customer"\nversion="0"\n') + (customer / "hatch.toml").write_text( + "[envs.review]\ndetached=true\ndependencies=[]\npython=" + json.dumps(python) + "\n" + ) + if version_file: + (customer / ".python-version").write_text(".".join(map(str, expected[1])) + "\n") + declarations = {path.name: path.read_bytes() for path in customer.iterdir()} + return customer, expected, declarations + + +def _absolute_target(tmp_path, monkeypatch, customer, hatch, existing): + for name in tuple(os.environ): + if name.startswith("HATCH_"): + monkeypatch.delenv(name) + monkeypatch.setenv("HATCH_DATA_DIR", str(tmp_path / "hatch-data")) + monkeypatch.setenv("HATCH_CACHE_DIR", str(tmp_path / "hatch-cache")) + environment = { + key: value + for key, value in os.environ.items() + if key not in {"PYTHONPATH", "PYTHONHOME", "VIRTUAL_ENV", "__PYVENV_LAUNCHER__"} + } + if existing: + subprocess.run( + [hatch, "env", "create", "review"], + cwd=customer, + env=environment, + check=True, + capture_output=True, + text=True, + timeout=180, + ) + return ( + Path( + subprocess.check_output([hatch, "env", "find", "review"], cwd=customer, env=environment, text=True).strip() + ) + if existing + else tmp_path / "invocation/environment" + ) + + +def _assert_absolute_identity(prepared, customer, expected_target, expected, existing): + assert prepared.identity.ownership == ("existing" if existing else "invocation") + assert prepared.identity.interpreter.prefix == expected_target + assert str(prepared.identity.interpreter.base_prefix) == expected[0] + assert list(prepared.identity.interpreter.version) == expected[1] + result = prepared.run( + ProjectCommand( + ( + "python", + "-c", + "import sys,json; print(json.dumps([sys.base_prefix,list(sys.version_info[:3])," + "sys.prefix,sys.executable]))", + ), + customer, + ), + timeout=10, + ) + assert result.outcome == "exited" and result.exit_code == 0 + assert json.loads(result.stdout) == [*expected, str(expected_target), str(prepared.identity.interpreter.executable)] + + +@pytest.mark.parametrize("version_file", [False, True], ids=["without-version-file", "with-version-file"]) +@pytest.mark.parametrize("existing", [False, True], ids=["cold", "existing"]) +def test_native_hatch_absolute_python_declaration(tmp_path, monkeypatch, existing, version_file): + hatch = os.environ.get("SPECFACT_HATCH_EXECUTABLE") + python = os.environ.get("SPECFACT_HATCH_PROJECT_PYTHON") + if not hatch or not python: + if os.environ.get("SPECFACT_HATCH_PROOF_WORKSPACE"): + missing = [ + name + for name in ("SPECFACT_HATCH_EXECUTABLE", "SPECFACT_HATCH_PROJECT_PYTHON") + if not os.environ.get(name) + ] + pytest.fail("Configured native acceptance cell requires: " + ", ".join(missing)) + pytest.skip("Native Hatch and project Python are required for absolute declaration acceptance") + customer, expected, declarations = _create_absolute_customer(tmp_path, python, version_file) + expected_target = _absolute_target(tmp_path, monkeypatch, customer, hatch, existing) + native_which = project_runtime.shutil.which + monkeypatch.setattr(project_runtime.shutil, "which", lambda name: native_which(name) if name == hatch else None) + plan = inspect_project(customer, ProjectSelection(environment="review", hatch=hatch)) + assert not plan.diagnostics + prepared = prepare_project(plan, tmp_path / "invocation") + assert isinstance(prepared, PreparedProject), prepared + _assert_absolute_identity(prepared, customer, expected_target, expected, existing) + assert {name: (customer / name).read_bytes() for name in declarations} == declarations + + +@pytest.mark.parametrize("case", ["workflow", "storage", "absolute"]) +@pytest.mark.parametrize( + "missing", + [ + ("SPECFACT_HATCH_EXECUTABLE",), + ("SPECFACT_HATCH_PROJECT_PYTHON",), + ("SPECFACT_HATCH_EXECUTABLE", "SPECFACT_HATCH_PROJECT_PYTHON"), + ], +) +def test_configured_native_cell_fails_missing_prerequisites(tmp_path, monkeypatch, case, missing): + workspace = tmp_path / "native-proof" + monkeypatch.setenv("SPECFACT_HATCH_PROOF_WORKSPACE", str(workspace)) + monkeypatch.setenv("SPECFACT_HATCH_EXECUTABLE", str(tmp_path / "unused-hatch")) + monkeypatch.setenv("SPECFACT_HATCH_PROJECT_PYTHON", str(tmp_path / "unused-python")) + for name in missing: + monkeypatch.delenv(name) + workflow, arguments = { + "workflow": (test_existing_and_cold_native_hatch_workflow, ()), + "storage": (test_native_hatch_customer_storage_survives_controller_context, (tmp_path, monkeypatch)), + "absolute": (test_native_hatch_absolute_python_declaration, (tmp_path, monkeypatch, False, False)), + }[case] + with pytest.raises(pytest.fail.Exception, match="Configured native acceptance cell requires:") as failure: + try: + workflow(*arguments) + except pytest.skip.Exception as skipped: + raise AssertionError("Configured native cell skipped missing prerequisites") from skipped + assert all(name in str(failure.value) for name in missing) + assert not workspace.exists() + assert not (tmp_path / "customer").exists() + assert not (tmp_path / "invocation").exists() diff --git a/tests/support/capsule_review_fixtures.py b/tests/support/capsule_review_fixtures.py index 24d9f169..a2eadef3 100644 --- a/tests/support/capsule_review_fixtures.py +++ b/tests/support/capsule_review_fixtures.py @@ -278,6 +278,9 @@ def deferral_worktree(tmp_path: Path, scenario): workflow.write_text( workflow_source if independent_review else workflow_source.split("\n independent-review:", 1)[0] ) + native = repository / ".github/workflows/native-pr-review.yml" + native_source = (REPO_ROOT / ".github/workflows/native-pr-review.yml").read_text() + native.write_text(native_source if independent_review else "name: missing-independent-review\n") files = [ "llms.txt", "docs/reference/commands.generated.json", @@ -308,9 +311,13 @@ def deferral_worktree(tmp_path: Path, scenario): (worktree / ".github/workflows/capsule-customer-execution.yml").write_text( workflow_source.split("\n independent-review:", 1)[0] ) + if independent_review == "unstaged_restore": + (worktree / ".github/workflows/native-pr-review.yml").write_text("name: missing-independent-review\n") _git(worktree, "add", ".") if independent_review == "unstaged_restore": (worktree / ".github/workflows/capsule-customer-execution.yml").write_text(workflow_source) + if independent_review == "unstaged_restore": + (worktree / ".github/workflows/native-pr-review.yml").write_text(native_source) return worktree diff --git a/tests/unit/scripts/test_native_pr_review.py b/tests/unit/scripts/test_native_pr_review.py new file mode 100644 index 00000000..549b15a2 --- /dev/null +++ b/tests/unit/scripts/test_native_pr_review.py @@ -0,0 +1,501 @@ +from __future__ import annotations + +import importlib.util +import tomllib +from copy import deepcopy +from pathlib import Path + +import pytest +import yaml + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT = ROOT / "scripts/native_pr_review.py" +COVERAGE_ROOTS = tuple(tomllib.loads((ROOT / "pyproject.toml").read_text())["tool"]["coverage"]["run"]["source"]) + + +def _step_named(steps, name): + return next(step for step in steps if step.get("name") == name) + + +@pytest.fixture(autouse=True) +def record_selector(request): + request.node.user_properties.append(("specfact.selector", request.node.nodeid)) + + +@pytest.fixture +def native_review(): + spec = importlib.util.spec_from_file_location("native_pr_review", SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.fixture +def complete(native_review): + return { + "head": "a" * 40, + "source_before": "b" * 40, + "source_after": "b" * 40, + "members": [{"id": member, "outcome": "COMPLETE", "findings": []} for member in native_review.REQUIRED_MEMBERS], + "policy_exit": 0, + } + + +def test_complete_native_review_accepts_exact_head(native_review, complete): + native_review._validate_receipt(complete, expected_head="a" * 40) + + +@pytest.mark.parametrize("fault", ["missing", "duplicate", "unknown", "stale", "source", "policy", "malformed"]) +def test_incomplete_native_review_cannot_pass(native_review, complete, fault): + receipt = deepcopy(complete) + if fault == "missing": + receipt["members"].pop() + if fault == "duplicate": + receipt["members"].append(receipt["members"][0]) + if fault == "unknown": + receipt["members"][0]["outcome"] = "UNKNOWN" + if fault == "stale": + receipt["head"] = "c" * 40 + if fault == "source": + receipt["source_after"] = "d" * 40 + if fault == "policy": + receipt["policy_exit"] = 1 + if fault == "malformed": + receipt["members"][0]["findings"] = None + with pytest.raises(ValueError): + native_review._validate_receipt(receipt, expected_head="a" * 40) + + +@pytest.mark.parametrize( + "finding", + [ + {"category": "tool_error", "severity": "warning"}, + {"category": "contracts", "evidence_outcome": "UNKNOWN"}, + {"category": "testing", "execution_state": "skipped"}, + ], +) +def test_raw_incomplete_findings_block_even_when_policy_exit_is_zero(native_review, complete, finding): + complete["members"][0]["findings"] = [finding] + with pytest.raises(ValueError, match="incomplete"): + native_review._validate_receipt(complete, expected_head="a" * 40) + + +def test_required_signed_rule_pack_cannot_silently_disappear(native_review, tmp_path): + with pytest.raises(ValueError, match="rule"): + native_review._rule_identities(tmp_path) + + +def test_type_projection_covers_exact_changed_python_inventory(native_review, tmp_path): + import json + + files = [Path("scripts/native_pr_review.py"), Path("tests/unit/scripts/test_native_pr_review.py")] + _, config, parsed = native_review._trusted_configuration(ROOT, "HEAD", tmp_path, files) + projected = json.loads(config.read_text()) + assert projected["include"] == [str(ROOT / path) for path in files] + assert projected["typeCheckingMode"] == parsed["tool"]["basedpyright"]["typeCheckingMode"] + assert projected["extraPaths"] == [str(ROOT / path) for path in parsed["tool"]["basedpyright"]["extraPaths"]] + + +def test_type_projection_retains_imported_type_errors(native_review, tmp_path, monkeypatch): + import json + import subprocess + + subject, output = tmp_path / "subject", tmp_path / "output" + subject.mkdir() + output.mkdir() + (subject / "helper.py").write_text("def value() -> str:\n return 'value'\n") + (subject / "changed.py").write_text("from helper import value\nnumber: int = value()\n") + (subject / "unrelated.py").write_text("number: int = 'wrong'\n") + monkeypatch.setattr(native_review, "_git", lambda *args: (ROOT / "pyproject.toml").read_text()) + _, config, _ = native_review._trusted_configuration(subject, "a" * 40, output, [Path("changed.py")]) + result = subprocess.run( + ["basedpyright", "--outputjson", "--project", str(config)], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + assert result.returncode == 1, result.stderr + diagnostics = json.loads(result.stdout)["generalDiagnostics"] + assert any(row["rule"] == "reportAssignmentType" for row in diagnostics) + assert {Path(row["file"]).name for row in diagnostics} == {"changed.py"} + + +def test_quality_requires_native_review_without_capsule_prerequisite(): + jobs = yaml.safe_load((ROOT / ".github/workflows/pr-orchestrator.yml").read_text())["jobs"] + assert "customer-capsules" not in jobs + required = {"verify-module-signatures", "native-scope", "native-hatch", "native-review"} + assert required <= set(jobs["quality"]["needs"]) + gate = next(step for step in jobs["quality"]["steps"] if step.get("name") == "Require independent native review") + assert gate["env"]["NATIVE_REVIEW_RESULT"] == "${{ needs.native-review.result }}" + assert '!= "success"' in gate["run"] + assert not gate.get("continue-on-error", False) + + +def _assert_native_review_environment(review): + assert "env -i" in review["run"] + assert 'python" -I ' in review["run"] + assert "native_pr_review.py" in review["run"] + assert "GITHUB_TOKEN" not in review.get("env", {}) + assert "SPECFACT_HATCH_PROOF_WORKSPACE=" in review["run"] + assert '$(dirname "$SPECFACT_HATCH_PROJECT_PYTHON")' in review["run"] + + +def test_independent_native_job_uses_signed_reviewer_and_exact_subject(): + workflow = yaml.safe_load((ROOT / ".github/workflows/native-pr-review.yml").read_text()) + assert workflow["permissions"] == {"contents": "read"} + job = workflow["jobs"]["independent-review"] + assert not job.get("continue-on-error", False) + steps = job["steps"] + checkout = _step_named(steps, "Checkout exact head") + assert checkout["with"]["ref"] == "${{ github.event.pull_request.head.sha || github.sha }}" + assert checkout["with"]["persist-credentials"] is False + install = _step_named(steps, "Install independent signed reviewer") + assert "specfact-cli==0.55.4" in install["run"] + assert "--version 0.51.2 --source marketplace" in install["run"] + review = _step_named(steps, "Run complete independent native review") + _assert_native_review_environment(review) + + +def test_all_signed_semgrep_passes_are_required(native_review, tmp_path): + rules = tmp_path / ".semgrep" + rules.mkdir() + for name in ("clean_code.yaml", "bugs.yaml"): + (rules / name).write_text("rules:\n - id: example\n") + with pytest.raises(ValueError, match="ai-bloat"): + native_review._rule_identities(tmp_path) + + +def test_immutable_core_registry_apis_are_checked_before_candidate_bootstrap(): + workflow = yaml.safe_load((ROOT / ".github/workflows/native-pr-review.yml").read_text()) + steps = workflow["jobs"]["independent-review"]["steps"] + name = "Verify immutable core registry APIs" + assert name in [step.get("name") for step in steps] + verify = _step_named(steps, name) + assert not verify.get("if") and not verify.get("continue-on-error", False) + assert steps.index(_step_named(steps, "Install independent signed reviewer")) < steps.index(verify) + assert steps.index(verify) < steps.index(_step_named(steps, "Bootstrap native repository environment")) + assert 'version("specfact-cli") == "0.55.4"' in verify["run"] + assert '"$trusted/venv/bin/python" -I -' in verify["run"] + assert "discover_all_modules_for_project_with_shadowed" in verify["run"] + assert "verify_module_artifact" in verify["run"] + + +def test_native_tools_are_installed_from_base_before_project_bootstrap(): + workflow = yaml.safe_load((ROOT / ".github/workflows/native-pr-review.yml").read_text()) + steps = workflow["jobs"]["independent-review"]["steps"] + install = _step_named(steps, "Install independent signed reviewer") + bootstrap = _step_named(steps, "Bootstrap native repository environment") + assert steps.index(install) < steps.index(bootstrap) + assert 'git show "$base:pyproject.toml"' in install["run"] + review = _step_named(steps, "Run complete independent native review") + assert 'PATH="$trusted/venv/bin:' in review["run"] + + +def test_semgrep_dependencies_are_separate_from_signed_controller(tmp_path): + import subprocess + import sys + + workflow = yaml.safe_load((ROOT / ".github/workflows/native-pr-review.yml").read_text()) + steps = workflow["jobs"]["independent-review"]["steps"] + install = _step_named(steps, "Install independent signed reviewer")["run"] + program = install.split("<<'PYTHON'\n", 1)[1].split("\nPYTHON", 1)[0] + (tmp_path / "base-pyproject.toml").write_bytes((ROOT / "pyproject.toml").read_bytes()) + lock = ( + ROOT + / "packages/specfact-code-review/src/specfact_code_review/resources/contracts/pr-range-v1-toolchain-lock.json" + ) + (tmp_path / "analyzer-lock.json").write_bytes(lock.read_bytes()) + subprocess.run([sys.executable, "-I", "-c", program, str(tmp_path)], check=True) + requirements = (tmp_path / "requirements.txt").read_text().splitlines() + assert not any(item.startswith("semgrep") for item in requirements), ( + "Semgrep and core require incompatible Click versions" + ) + assert (tmp_path / "semgrep-requirement.txt").read_text().strip() == "semgrep==1.144.0" + assert '"$trusted/semgrep/bin/python" -I -m pip install -r "$trusted/semgrep-requirement.txt"' in install + constraints = (tmp_path / "semgrep-constraints.txt").read_text().splitlines() + assert "pydantic==2.13.4" in constraints + assert "mcp==1.16.0" in constraints + assert '-c "$trusted/semgrep-constraints.txt"' in install + review = _step_named(steps, "Run complete independent native review")["run"] + assert 'PATH="$trusted/venv/bin:$trusted/semgrep/bin:' in review + + +def test_semgrep_version_comes_from_selected_executable(native_review, monkeypatch): + import subprocess + + def version(command, **kwargs): + assert command == ["semgrep", "--version"] + assert kwargs["check"] and kwargs["timeout"] == 30 + return subprocess.CompletedProcess(command, 0, "1.144.0\n", "") + + monkeypatch.setattr(native_review.subprocess, "run", version) + monkeypatch.setattr(native_review.importlib.metadata, "version", lambda name: "controller-version") + versions = native_review._tool_versions() + assert versions["semgrep"] == "1.144.0" + assert versions["specfact-cli"] == "controller-version" + + +def test_semgrep_startup_failure_preserves_bounded_stderr(native_review, monkeypatch): + import subprocess + + def fail(*args, **kwargs): + raise subprocess.CalledProcessError( + 1, ["semgrep", "--version"], stderr="x" * 3000 + "missing dependency symbol" + ) + + monkeypatch.setattr(native_review.subprocess, "run", fail) + monkeypatch.setattr(native_review.importlib.metadata, "version", lambda name: "controller-version") + with pytest.raises(ValueError, match="missing dependency symbol") as error: + native_review._tool_versions() + assert len(str(error.value)) < 2100 + + +@pytest.mark.parametrize("commit", ["046d366c190af7a188d6f48953a96edb65e766db", "0" * 40]) +def test_runtime_smoke_rejects_unreviewed_fixture_identity(tmp_path, commit): + import json + import subprocess + import sys + + jobs = yaml.safe_load((ROOT / ".github/workflows/pr-orchestrator.yml").read_text())["jobs"] + verify = _step_named(jobs["quality"]["steps"], "Verify reviewed runtime smoke fixture") + program = verify["run"].split("<<'PYTHON'\n", 1)[1].split("\nPYTHON", 1)[0] + lock = tmp_path / "specfact-cli/ci/runtime-smoke-fixture.lock.json" + lock.parent.mkdir(parents=True) + lock.write_text( + json.dumps( + { + "repository": "nold-ai/specfact-cli-modules", + "commit": commit, + "tree": "c5b44db3477d1c9ea8dae1dceab2af276d265353", + } + ) + ) + result = subprocess.run( + [sys.executable, "-I", "-c", program], cwd=tmp_path, capture_output=True, text=True, check=False + ) + assert (result.returncode == 0) == (commit != "0" * 40), result.stderr + + +def test_runtime_smoke_preserves_launchers_and_isolates_dependencies(): + jobs = yaml.safe_load((ROOT / ".github/workflows/pr-orchestrator.yml").read_text())["jobs"] + steps = jobs["quality"]["steps"] + checkout = _step_named(steps, "Checkout reviewed runtime smoke fixture") + assert checkout["with"]["ref"] == "046d366c190af7a188d6f48953a96edb65e766db" + assert checkout["with"]["persist-credentials"] is False + smoke = _step_named(steps, "Runtime discovery smoke across package managers") + assert steps.index(checkout) < steps.index(smoke) + for launcher in ("direct", "hatch-source", "pip-editable", "pipx", "uv-run", "uvx"): + assert "--launcher " + launcher in smoke["run"] + assert 'python -m venv "$launchers"' in smoke["run"] + assert '"$launchers/bin/python" -m pip install uv' in smoke["run"] + assert '--modules-repo "$GITHUB_WORKSPACE/specfact-cli/runtime-smoke-modules"' in smoke["run"] + + +def test_runtime_smoke_seeds_frozen_cache_before_offline_execution(): + jobs = yaml.safe_load((ROOT / ".github/workflows/pr-orchestrator.yml").read_text())["jobs"] + command = _step_named(jobs["quality"]["steps"], "Runtime discovery smoke across package managers")["run"] + assert "check_dependency_trust_exceptions.py" in command + assert "sync --locked --all-extras --no-install-project --project specfact-cli" in command + assert ( + command.index("check_dependency_trust_exceptions.py") + < command.index("sync --locked") + < command.index("runtime_discovery_smoke.py") + ) + assert 'UV_PROJECT_ENVIRONMENT="$RUNNER_TEMP/runtime-smoke-seed"' in command + assert 'PATH="$RUNNER_TEMP/runtime-smoke-seed/bin:$launchers/bin:$PATH"' in command + assert "virtualenv<21" not in command + + +def test_existing_nonmirrored_package_tests_are_selected(native_review): + inventory = native_review._test_inventory( + ROOT, [Path("packages/specfact-codebase/src/specfact_codebase/import_cmd/commands.py")], roots=COVERAGE_ROOTS + ) + assert "tests/unit/specfact_codebase/test_import_command_contract.py" in inventory + assert all((ROOT / path).is_file() for path in inventory) + + +def test_missing_package_tests_cannot_pass_selection(native_review, tmp_path): + with pytest.raises(ValueError, match="test"): + native_review._test_inventory( + tmp_path, + [Path("packages/specfact-codebase/src/specfact_codebase/import_cmd/commands.py")], + roots=COVERAGE_ROOTS, + ) + + +@pytest.mark.parametrize( + "source,expected", + [ + ("tools/contract_first_smart_test.py", "tests/unit/tools/test_contract_first_smart_test.py"), + ("src/specfact_cli_modules/dev_bootstrap.py", "tests/unit/test_dev_bootstrap.py"), + ], + ids=["tool", "top-level-src"], +) +def test_coverage_roots_select_existing_source_tests(native_review, source, expected): + inventory = native_review._test_inventory(ROOT, [Path(source)], roots=COVERAGE_ROOTS) + assert expected in inventory + + +def test_nonmirrored_tool_selects_its_existing_test_files(native_review): + inventory = native_review._test_inventory(ROOT, [Path("tools/smart_test_coverage.py")], roots=COVERAGE_ROOTS) + assert set(inventory) == { + "tests/unit/test_capsule_proof_contexts.py", + "tests/unit/specfact_code_review/run/test_reviewer_bootstrap.py", + } + + +@pytest.mark.parametrize("missing", ["context", "bootstrap"]) +def test_nonmirrored_tool_rejects_missing_mapped_test(native_review, tmp_path, missing): + tests = { + "context": "tests/unit/test_capsule_proof_contexts.py", + "bootstrap": "tests/unit/specfact_code_review/run/test_reviewer_bootstrap.py", + } + for name, file in tests.items(): + if name != missing: + path = tmp_path / file + path.parent.mkdir(parents=True) + path.write_text("def test_existing(): pass\n") + with pytest.raises(ValueError, match="test"): + native_review._test_inventory(tmp_path, [Path("tools/smart_test_coverage.py")], roots=COVERAGE_ROOTS) + + +def test__native_test_command_uses_trusted_config(native_review, tmp_path): + command = native_review._native_test_command( + python=Path("/project/python"), + config=tmp_path / "pyproject.toml", + output=tmp_path, + ) + assert command[:3] == ["/project/python", "-m", "pytest"] + assert "--rootdir=." in command + assert command[command.index("-c") + 1] == str(tmp_path / "pyproject.toml") + assert f"--cov-config={tmp_path / 'pyproject.toml'}" in command + assert "tests/host/proof_capsule_deferred_review_ci.py" in command + assert "tests" not in command, "A parent directory can discard explicitly selected proof files" + + +def test_darwin_deferral_requires_indexed_native_replacement(tmp_path): + import os + import subprocess + + from tests.support.capsule_review_fixtures import ( + _BLOCK2_HATCH_FIXTURE, + _git, + deferral_worktree, + ) + + scenario = ("Darwin", "", "github-linux", "specfact-code-review", False, True, 0) + worktree = deferral_worktree(tmp_path, scenario) + workflow = worktree / ".github/workflows/native-pr-review.yml" + workflow.write_text("name: missing-independent-native-review\n") + _git(worktree, "add", ".") + calls = tmp_path / "calls" + script = (ROOT / "scripts/pre-commit-quality-checks.sh").read_text().rsplit('main "$@"', 1)[0] + result = subprocess.run( + ["bash", "-c", script + _BLOCK2_HATCH_FIXTURE], + cwd=worktree, + env=os.environ + | { + "FIXTURE_PLATFORM": "Darwin", + "FIXTURE_CALLS": str(calls), + "GITHUB_ACTIONS": "", + "CI": "", + "SPECFACT_CODE_REVIEW_DEFER_TO_CI": "github-linux", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1, result.stderr + assert "native" in result.stderr.lower() + + +def test_project_tests_do_not_inherit_controller_diff_mode(native_review, monkeypatch): + monkeypatch.setenv("SPECFACT_CODE_REVIEW_CHANGED_DIFF", "cached") + monkeypatch.setenv("SPECFACT_HATCH_PROJECT_PYTHON", "/project/python") + environment = native_review._native_test_environment() + assert "SPECFACT_CODE_REVIEW_CHANGED_DIFF" not in environment + assert environment["SPECFACT_HATCH_PROJECT_PYTHON"] == "/project/python" + + +def test_subject_detects_assume_unchanged_mutation(native_review, tmp_path, monkeypatch): + import subprocess + + def git(*args): + return subprocess.check_output(["git", "-C", str(tmp_path), *args], text=True).strip() + + git("init", "-q") + git("config", "user.name", "native-ci-fixture") + git("config", "user.email", "native-ci@example.invalid") + git("config", "commit.gpgsign", "false") + git("config", "core.hooksPath", "/dev/null") + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + git("add", ".") + git("commit", "-qm", "base") + base = git("rev-parse", "HEAD") + source.write_text("VALUE = 2\n") + git("add", ".") + git("commit", "-qm", "head") + head = git("rev-parse", "HEAD") + git("reset", "--soft", base) + monkeypatch.chdir(tmp_path) + before, _ = native_review._subject(tmp_path, head, base) + git("update-index", "--assume-unchanged", "--", "app.py") + source.write_text("VALUE = 3\n") + after, _ = native_review._subject(tmp_path, head, base) + assert before != after, "Git assume-unchanged hid a real source mutation" + + +def test_no_python_sources_requires_positive_applicability(native_review, complete): + complete["files"] = {} + for member in complete["members"][:-1]: + member.update(outcome="NOT_APPLICABLE", reason="no_changed_python_sources") + native_review._validate_receipt(complete, expected_head="a" * 40) + complete["files"] = {"changed.py": "hash"} + with pytest.raises(ValueError): + native_review._validate_receipt(complete, expected_head="a" * 40) + + +def test_boundary_script_imports_through_native_contract_analyzer(): + import subprocess + import sys + + result = subprocess.run( + [ + sys.executable, + "-c", + "from crosshair.util import load_file; from pathlib import Path; " + 'load_file(Path("scripts/check-bundle-imports.py"))', + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + +def test_targeted_source_test_skip_is_incomplete(native_review, tmp_path): + report = tmp_path / "tests.xml" + report.write_text(""" + + + + """) + with pytest.raises(ValueError, match="skipped"): + native_review._require_native_test_evidence(report) + + +def test_selected_test_file_cannot_disappear_from_evidence(native_review, tmp_path): + report = tmp_path / "tests.xml" + report.write_text(""" + + + """) + with pytest.raises(ValueError, match="test_changed_source"): + native_review._require_native_test_evidence(report, selectors=("tests/unit/test_changed_source.py",)) diff --git a/tests/unit/scripts/test_native_review_scope.py b/tests/unit/scripts/test_native_review_scope.py new file mode 100644 index 00000000..845ebaff --- /dev/null +++ b/tests/unit/scripts/test_native_review_scope.py @@ -0,0 +1,291 @@ +from __future__ import annotations + +import hashlib +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + + +ROOT = Path(__file__).resolve().parents[3] +LEGACY = ".github/workflows/capsule-customer-execution.yml" + + +def git(root: Path, *args: str) -> str: + return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip() + + +@pytest.fixture(autouse=True) +def record_selector(request, record_property): + record_property("specfact.selector", request.node.nodeid) + + +@pytest.fixture(name="repository") +def scope_repository(tmp_path: Path) -> Path: + (tmp_path / "scripts").mkdir() + for name in ("check-bundle-imports.py", "check_native_review_scope.py"): + source = ROOT / "scripts" / name + if source.exists(): + shutil.copyfile(source, tmp_path / "scripts" / name) + legacy = tmp_path / LEGACY + legacy.parent.mkdir(parents=True) + legacy.write_bytes((ROOT / LEGACY).read_bytes()) + runtime = tmp_path / "packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py" + runtime.parent.mkdir(parents=True) + runtime.write_text("VALUE = 1\n") + test = tmp_path / "tests/unit/test_capsule_review_projection.py" + test.parent.mkdir(parents=True) + test.write_text("def test_existing():\n assert True\n") + git(tmp_path, "init", "-q") + git(tmp_path, "config", "user.name", "scope-fixture") + git(tmp_path, "config", "user.email", "scope-fixture@example.invalid") + git(tmp_path, "config", "commit.gpgsign", "false") + git(tmp_path, "config", "core.hooksPath", "/dev/null") + git(tmp_path, "add", ".") + git(tmp_path, "commit", "-qm", "baseline") + policy = { + "execution_backend": "trusted-local-native", + "freeze_ref": git(tmp_path, "rev-parse", "HEAD"), + "legacy_paths": [ + LEGACY, + "packages/specfact-code-review/src/specfact_code_review/run/*", + "tests/unit/test_capsule*", + ], + "baseline_files": { + name: [mode, oid] + for row in git(tmp_path, "ls-tree", "-r", "HEAD").splitlines() + for metadata, name in [row.split("\t", 1)] + for mode, _, oid in [metadata.split()] + }, + } + (tmp_path / "openspec").mkdir() + policy_path = tmp_path / "openspec/native-review-policy.json" + policy_path.write_text(json.dumps(policy)) + checker = tmp_path / "scripts/check_native_review_scope.py" + if checker.exists(): + approved_digest = hashlib.sha256((ROOT / "openspec/native-review-policy.json").read_bytes()).hexdigest() + fixture_digest = hashlib.sha256(policy_path.read_bytes()).hexdigest() + checker.write_text(checker.read_text().replace(approved_digest, fixture_digest)) + return tmp_path + + +def check(root: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(root / "scripts/check-bundle-imports.py")], + cwd=root, + capture_output=True, + text=True, + check=False, + ) + + +def rewrite_reviewed_policy(root: Path, field: str, value: object) -> None: + path = root / "openspec/native-review-policy.json" + old_digest = hashlib.sha256(path.read_bytes()).hexdigest() + policy = json.loads(path.read_text()) + policy[field] = value + path.write_text(json.dumps(policy)) + checker = root / "scripts/check_native_review_scope.py" + checker.write_text(checker.read_text().replace(old_digest, hashlib.sha256(path.read_bytes()).hexdigest())) + + +@pytest.mark.parametrize( + "path", + [ + LEGACY, + "packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py", + "tests/unit/test_capsule_review_projection.py", + "packages/specfact-code-review/src/specfact_code_review/run/new_capsule.py", + ], + ids=["workflow", "runtime", "test", "untracked"], +) +def test_boundary_rejects_capsule_changes(repository: Path, path: str) -> None: + target = repository / path + target.write_text(target.read_text() + "# capsule repair\n" if target.exists() else "VALUE = 2\n") + result = check(repository) + assert result.returncode == 1, result.stdout + result.stderr + assert path in result.stdout + result.stderr + assert "native" in (result.stdout + result.stderr).lower() + + +@pytest.mark.parametrize("change", ["unchanged", "native", "retirement"]) +def test_boundary_preserves_native_work_and_retirement(repository: Path, change: str) -> None: + if change == "native": + target = repository / "packages/specfact-codebase/src/specfact_codebase/project_runtime.py" + target.parent.mkdir(parents=True) + target.write_text("VALUE = 2\n") + elif change == "retirement": + (repository / LEGACY).unlink() + result = check(repository) + assert result.returncode == 0, result.stdout + result.stderr + + +@pytest.mark.parametrize("change", ["unchanged", "native", "retirement", "repair", "addition", "mode"]) +def test_native_scope_survives_squash_only_checkout(repository: Path, tmp_path: Path, change: str) -> None: + final_tree = tmp_path / "squashed" + shutil.copytree(repository, final_tree, ignore=shutil.ignore_patterns(".git", "squashed")) + git(final_tree, "init", "-q") + git(final_tree, "config", "user.name", "scope-fixture") + git(final_tree, "config", "user.email", "scope-fixture@example.invalid") + git(final_tree, "config", "commit.gpgsign", "false") + git(final_tree, "config", "core.hooksPath", "/dev/null") + git(final_tree, "add", ".") + git(final_tree, "commit", "-qm", "single retained final tree") + frozen = json.loads((final_tree / "openspec/native-review-policy.json").read_text())["freeze_ref"] + absent = subprocess.run(["git", "-C", str(final_tree), "cat-file", "-e", frozen], capture_output=True, check=False) + assert absent.returncode != 0, "Fixture unexpectedly retained the pre-squash feature commit" + apply_squash_change(final_tree, change) + result = check(final_tree) + assert result.returncode == (0 if change in {"unchanged", "native", "retirement"} else 1), ( + result.stdout + result.stderr + ) + assert "freeze_ref comparison unavailable" not in result.stdout + result.stderr + if change in {"repair", "addition", "mode"}: + assert "capsule" in result.stdout + result.stderr + + +def apply_squash_change(root: Path, change: str) -> None: + if change == "native": + (root / "native.py").write_text("VALUE = 1\n") + return + if change == "retirement": + (root / LEGACY).unlink() + return + if change == "repair": + (root / LEGACY).write_text("# capsule repair\n") + return + if change == "addition": + (root / "tests/unit/test_capsule_new.py").write_text("VALUE = 1\n") + return + if change == "mode": + (root / LEGACY).chmod(0o755) + + +def test_native_scope_preserves_git_checkout_normalization(repository: Path) -> None: + git(repository, "config", "core.autocrlf", "true") + path = repository / LEGACY + path.write_bytes(path.read_bytes().replace(b"\n", b"\r\n")) + assert git(repository, "diff", "--name-only", "HEAD", "--", LEGACY) == "" + result = check(repository) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_native_scope_rejects_staged_mode_when_filemode_disabled(repository: Path) -> None: + git(repository, "config", "core.filemode", "false") + git(repository, "update-index", "--chmod=+x", "--", LEGACY) + assert LEGACY in git(repository, "diff", "--name-only", "HEAD", "--", LEGACY) + result = check(repository) + assert result.returncode == 1, result.stdout + result.stderr + assert LEGACY in result.stdout + + +@pytest.mark.parametrize( + "field,value", + [("execution_backend", "capsule"), ("freeze_ref", "missing-revision")], + ids=["backend", "revision"], +) +def test_boundary_rejects_invalid_policy(repository: Path, field: str, value: str) -> None: + rewrite_reviewed_policy(repository, field, value) + result = check(repository) + assert result.returncode == 1, result.stdout + result.stderr + expected = "execution_backend must remain" if field == "execution_backend" else "freeze_ref" + assert expected in result.stdout + result.stderr + + +def test_boundary_rejects_actual_historical_capsule_repair(repository: Path) -> None: + history = json.loads((ROOT / "tests/fixtures/native-review-scope-history.json").read_text()) + (repository / LEGACY).write_text(history["before"]) + git(repository, "add", LEGACY) + git(repository, "commit", "-qm", "original historical excerpt") + before = git(repository, "rev-parse", "HEAD") + (repository / LEGACY).write_text(history["after"]) + git(repository, "add", LEGACY) + git(repository, "commit", "-qm", "actual historical repair excerpt") + after = git(repository, "rev-parse", "HEAD") + result = subprocess.run( + [ + sys.executable, + str(repository / "scripts/check_native_review_scope.py"), + f"--base-ref={before}", + f"--head-ref={after}", + ], + cwd=repository, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1, result.stdout + result.stderr + assert LEGACY in result.stdout + result.stderr + + +@pytest.mark.parametrize("weakening", ["baseline", "paths"]) +def test_boundary_rejects_policy_weakening(repository: Path, weakening: str) -> None: + (repository / LEGACY).write_text("# capsule repair\n") + git(repository, "add", LEGACY) + git(repository, "commit", "-qm", "capsule repair") + path = repository / "openspec/native-review-policy.json" + policy = json.loads(path.read_text()) + if weakening == "baseline": + policy["freeze_ref"] = git(repository, "rev-parse", "HEAD") + else: + policy["legacy_paths"].remove(LEGACY) + path.write_text(json.dumps(policy)) + result = check(repository) + assert result.returncode == 1, result.stdout + result.stderr + assert "policy" in result.stdout + result.stderr + + +@pytest.mark.parametrize("source", ["policy", "head"]) +def test_revision_options_cannot_write_files(repository: Path, source: str) -> None: + output = repository / "unexpected-git-output" + option = f"--output={output}" + if source == "policy": + rewrite_reviewed_policy(repository, "freeze_ref", option) + result = check(repository) + else: + result = subprocess.run( + [sys.executable, str(repository / "scripts/check_native_review_scope.py"), f"--head-ref={option}"], + cwd=repository, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1, result.stdout + result.stderr + assert not output.exists(), "Git interpreted a revision as a file-writing option" + + +def test_native_scope_ci_is_independent_and_read_only() -> None: + path = ROOT / ".github/workflows/native-review-scope.yml" + assert path.is_file(), "Required native scope CI job is missing" + workflow = yaml.safe_load(path.read_text()) + assert workflow["permissions"] == {"contents": "read"} + job = workflow["jobs"]["native-scope"] + assert not job.get("needs") + assert not job.get("continue-on-error", False) + assert "python scripts/check_native_review_scope.py" in [step.get("run") for step in job["steps"]] + checkout = checkout_step(job) + assert checkout["with"] == {"fetch-depth": 0, "persist-credentials": False} + assert_native_scope_prerequisite() + + +def assert_native_scope_prerequisite(): + orchestrator = yaml.safe_load((ROOT / ".github/workflows/pr-orchestrator.yml").read_text()) + assert "native-scope" in orchestrator["jobs"], "Required native scope prerequisite is missing" + gate = orchestrator["jobs"]["native-scope"] + assert gate["uses"] == "./.github/workflows/native-review-scope.yml" + assert "native-scope" in orchestrator["jobs"]["quality"]["needs"] + prerequisite = named_step(orchestrator["jobs"]["quality"], "Require native migration scope") + assert prerequisite["env"]["NATIVE_SCOPE_RESULT"] == "${{ needs.native-scope.result }}" + assert '"$NATIVE_SCOPE_RESULT" != "success"' in prerequisite["run"] + + +def checkout_step(job): + return next(step for step in job["steps"] if step.get("uses", "").startswith("actions/checkout@")) + + +def named_step(job, name): + return next(step for step in job["steps"] if step.get("name") == name) diff --git a/tests/unit/specfact_code_review/run/test_runtime_compatibility.py b/tests/unit/specfact_code_review/run/test_runtime_compatibility.py index ee02c57c..16d63b0c 100644 --- a/tests/unit/specfact_code_review/run/test_runtime_compatibility.py +++ b/tests/unit/specfact_code_review/run/test_runtime_compatibility.py @@ -2,11 +2,13 @@ import importlib.util import json +import os import shutil import tomllib from importlib import import_module from importlib.metadata import distribution from pathlib import Path +from urllib.parse import unquote, urlsplit import pytest from packaging.requirements import Requirement @@ -18,6 +20,11 @@ from specfact_code_review.run.runtime_domains import member_dependency_graphs +@pytest.fixture(autouse=True) +def record_selector(request): + request.node.user_properties.append(("specfact.selector", request.node.nodeid)) + + def test_incompatible_dependency_identifies_only_affected_member(tmp_path: Path) -> None: metadata = tmp_path / "pylint-4.0.7.dist-info" @@ -193,11 +200,80 @@ def test_declared_core_dependency_supplies_registry_handoff() -> None: declared = _declared_development_dependencies(root) requirement = declared.get("specfact-cli") assert requirement is not None, "Core installed by developer bootstrap is not a declared target dependency" + assert str(requirement.specifier) == "==0.55.4", "Declare the verified immutable core release" installed = distribution("specfact-cli") - assert str(requirement.specifier) == f"=={installed.version}", "Declare the verified immutable core release" + paired = os.environ.get("SPECFACT_CLI_REPO") + core = Path(paired).resolve() if paired else None + if core is None: + assert installed.version == "0.55.4", "Unpaired review requires the declared immutable release" + else: + provenance = json.loads(installed.read_text("direct_url.json") or "{}") + location = urlsplit(provenance.get("url", "")) + assert provenance.get("dir_info", {}).get("editable") is True, "Paired core must be installed editable" + assert location.scheme == "file" and not location.netloc, "Paired core must be a local checkout" + assert Path(unquote(location.path)).resolve() == core, "Installed core differs from the selected checkout" + declared_version = tomllib.loads((core / "pyproject.toml").read_text())["project"]["version"] + assert installed.version == declared_version, "Installed core metadata differs from its checkout" + package = import_module("specfact_cli") + assert package.__file__ and Path(package.__file__).resolve() == core / "src/specfact_cli/__init__.py" for module_name, entry in ( ("module_discovery", "discover_all_modules_for_project_with_shadowed"), ("module_installer", "verify_module_artifact"), ): module = import_module(f"specfact_cli.registry.{module_name}") assert callable(getattr(module, entry)), f"Declared core does not supply registry API {entry}" + if core is not None: + assert module.__file__ and Path(module.__file__).resolve().is_relative_to(core / "src/specfact_cli") + + +@pytest.fixture +def paired_core_facts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + from types import SimpleNamespace + + core = tmp_path / "paired-core" + source = core / "src/specfact_cli/registry" + source.mkdir(parents=True) + (core / "pyproject.toml").write_text('[project]\nversion = "0.55.4"\n') + metadata = {"dir_info": {"editable": True}, "url": core.as_uri()} + installed = SimpleNamespace(version="0.55.4", read_text=lambda name: json.dumps(metadata)) + modules = {"specfact_cli": SimpleNamespace(__file__=str(source.parent / "__init__.py"))} + for name, entry in ( + ("module_discovery", "discover_all_modules_for_project_with_shadowed"), + ("module_installer", "verify_module_artifact"), + ): + path = source / f"{name}.py" + path.touch() + modules[f"specfact_cli.registry.{name}"] = SimpleNamespace(__file__=str(path), **{entry: lambda: None}) + monkeypatch.setenv("SPECFACT_CLI_REPO", str(core)) + monkeypatch.setattr(__name__ + ".distribution", lambda name: installed) + monkeypatch.setattr(__name__ + ".import_module", modules.__getitem__) + return core, installed, metadata, modules + + +def test_explicit_paired_core_keeps_registry_handoff(paired_core_facts): + core, installed, _, _ = paired_core_facts + installed.version = "0.55.11" + (core / "pyproject.toml").write_text('[project]\nversion = "0.55.11"\n') + test_declared_core_dependency_supplies_registry_handoff() + + +@pytest.mark.parametrize( + "fault", ["noneditable", "other-checkout", "core-origin", "registry-origin", "version", "unpaired"] +) +def test_paired_core_registry_handoff_rejects_unrelated_installation(paired_core_facts, tmp_path, monkeypatch, fault): + _, installed, metadata, modules = paired_core_facts + if fault == "noneditable": + metadata["dir_info"]["editable"] = False + if fault == "other-checkout": + metadata["url"] = tmp_path.as_uri() + if fault == "registry-origin": + modules["specfact_cli.registry.module_installer"].__file__ = str(tmp_path / "module_installer.py") + if fault == "core-origin": + modules["specfact_cli"].__file__ = str(tmp_path / "__init__.py") + if fault == "version": + installed.version = "0.55.11" + if fault == "unpaired": + installed.version = "0.55.11" + monkeypatch.delenv("SPECFACT_CLI_REPO") + with pytest.raises(AssertionError): + test_declared_core_dependency_supplies_registry_handoff() diff --git a/tests/unit/specfact_codebase/test_project_runtime.py b/tests/unit/specfact_codebase/test_project_runtime.py new file mode 100644 index 00000000..82063567 --- /dev/null +++ b/tests/unit/specfact_codebase/test_project_runtime.py @@ -0,0 +1,696 @@ +import contextlib +import json +import os +import signal +import subprocess +import sys +from dataclasses import replace +from pathlib import Path +from sys import _base_executable as base_executable +from unittest.mock import MagicMock, Mock + +import pytest + +from specfact_codebase import project_runtime +from specfact_codebase.project_runtime import ( + PreparationFailure, + PreparedProject, + ProjectCommand, + ProjectSelection, + _probe as probe_python, + inspect_project, + prepare_project, +) + + +@pytest.fixture(autouse=True) +def lifecycle_selector(request, record_property): + record_property("specfact.selector", request.node.nodeid) + + +@pytest.fixture(name="project") +def customer_project(tmp_path): + root = tmp_path / "customer" + root.mkdir() + (root / "pyproject.toml").write_text('[project]\nname="customer"\nversion="0"\n') + (root / "hatch.toml").write_text( + '[envs.default]\ndependencies=["pytest<8"]\n[envs.review]\ndetached=true\ndependencies=[]\n' + ) + target = tmp_path / "existing" + subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(target)], check=True) + log = tmp_path / "hatch.log" + hatch = tmp_path / "hatch" + hatch.write_text( + f"#!{sys.executable}\nimport sys\nfrom pathlib import Path\n" + f'with Path({str(log)!r}).open("a") as f: f.write(" ".join(sys.argv[1:])+"\\n")\n' + f'print("Hatch, version test" if sys.argv[1:]==["--version"] else {str(target)!r})\n' + ) + hatch.chmod(0o700) + return root, target, hatch, log + + +def ready(project, tmp_path): + root, target, hatch, _ = project + plan = inspect_project( + root, ProjectSelection(environment="review", python=str(target / "bin/python"), hatch=str(hatch)) + ) + prepared = prepare_project(plan, tmp_path / "invocation") + assert isinstance(prepared, PreparedProject), prepared + return prepared + + +def test_inspection_is_pure_and_ambiguity_is_actionable(project): + root, _, hatch, log = project + plan = inspect_project(root, ProjectSelection(hatch=str(hatch))) + assert not log.exists() + assert "review" in str(plan.diagnostics) + assert "default" in str(plan.diagnostics) + + +def test_selected_venv_identity_and_no_sync_even_with_stale_dependencies(project, tmp_path): + root, target, _, log = project + config = root / "hatch.toml" + config.write_text( + config.read_text().replace("dependencies=[]", 'dependencies=["impossible-project-dependency==999"]') + ) + prepared = ready(project, tmp_path) + result = prepared.run( + ProjectCommand(("python", "-c", "import sys,json; print(json.dumps([sys.prefix,sys.executable]))"), root), + timeout=10, + ) + assert result.outcome == "exited" + assert json.loads(result.stdout) == [str(target), str(target / "bin/python")] + assert "create" not in log.read_text() + assert "run" not in log.read_text() + + +def test_directory_alias_retargeting_invalidates_prepared_identity(project, tmp_path): + root = project[0] + for name, contents in (("first", "[pytest]\naddopts=-q\n"), ("second", "[pytest]\naddopts=-v\n")): + target = root / name + target.mkdir() + (target / "pytest.ini").write_text(contents) + alias = root / "tests" + alias.symlink_to("first", target_is_directory=True) + prepared = ready(project, tmp_path) + before = (project_runtime._identity(root), project_runtime._identity(root, source=True)) + alias.unlink() + alias.symlink_to("second", target_is_directory=True) + after = (project_runtime._identity(root), project_runtime._identity(root, source=True)) + assert all(old != new for old, new in zip(before, after, strict=True)), (before, after) + result = prepared.run(ProjectCommand(("python", "-c", "print('command-ran')"), root), timeout=2) + assert result.outcome == "execution_failed" + assert "command-ran" not in result.stdout + + +def test_directory_alias_target_configuration_remains_fingerprinted(project, tmp_path): + root = project[0] + target = root / "selected" + target.mkdir() + config = target / "pytest.ini" + config.write_text("[pytest]\naddopts=-q\n") + (root / "tests").symlink_to("selected", target_is_directory=True) + prepared = ready(project, tmp_path) + config.write_text("[pytest]\naddopts=-v\n") + result = prepared.run(ProjectCommand(("python", "-c", "print('command-ran')"), root), timeout=2) + assert result.outcome == "execution_failed" + assert "command-ran" not in result.stdout + + +def test_missing_tool_never_falls_back_to_controller_path(project, tmp_path, monkeypatch): + prepared = ready(project, tmp_path) + shadow = tmp_path / "controller-bin" + shadow.mkdir() + executable = shadow / "definitely-missing-customer-tool" + executable.write_text("#!/bin/sh\necho controller-tool-executed\n") + executable.chmod(0o700) + monkeypatch.setenv("PATH", str(shadow)) + result = prepared.run(ProjectCommand(("definitely-missing-customer-tool",), project[0]), timeout=10) + assert result.outcome == "unavailable" + assert result.exit_code is None + + +def test_controller_python_path_removed_and_explicit_override_honored(project, tmp_path, monkeypatch): + prepared = ready(project, tmp_path) + monkeypatch.setenv("PYTHONPATH", "/controller/contamination") + monkeypatch.setenv("PYTHONHOME", "/controller/broken") + result = prepared.run( + ProjectCommand( + ("python", "-c", "import os; print(os.getenv('PYTHONPATH')); print(os.getenv('CUSTOM'))"), + project[0], + {"CUSTOM": "customer"}, + ), + timeout=10, + ) + assert result.stdout == "None\ncustomer\n" + refused = prepared.run(ProjectCommand(("python", "-V"), project[0], {"PATH": "/controller"}), timeout=10) + assert refused.outcome == "execution_failed" + + +def test_nonzero_timeout_and_bounded_output_are_distinct(project, tmp_path): + prepared = ready(project, tmp_path) + failed = prepared.run(ProjectCommand(("python", "-c", "raise SystemExit(7)"), project[0]), timeout=10) + assert failed.outcome == "exited" and failed.exit_code == 7 + timed = prepared.run(ProjectCommand(("python", "-c", "import time; time.sleep(5)"), project[0]), timeout=0.05) + assert timed.outcome == "timed_out" and timed.exit_code is None + huge = prepared.run(ProjectCommand(("python", "-c", "print('a'*2000000)"), project[0]), timeout=10) + assert huge.outcome == "exited" and len(huge.stdout) <= 65536 + assert huge.output_truncated + + descendant_pid = tmp_path / "detached.pid" + child_code = ( + "import subprocess,sys; from pathlib import Path; " + "child=subprocess.Popen([sys.executable,'-c','import time; time.sleep(8)'],start_new_session=True); " + f"Path({str(descendant_pid)!r}).write_text(str(child.pid)); " + "print('parent-finished',flush=True)" + ) + root, target, hatch, _ = project + watchdog_code = ( + "import time; from pathlib import Path; from specfact_codebase.project_runtime import " + "ProjectSelection,ProjectCommand,PreparedProject,inspect_project,prepare_project; " + f"root=Path({str(root)!r}); " + f"selection=ProjectSelection(environment='review',python={str(target / 'bin/python')!r},hatch={str(hatch)!r}); " + f"project=prepare_project(inspect_project(root,selection),Path({str(tmp_path / 'watchdog-invocation')!r})); " + "assert isinstance(project,PreparedProject); " + "started=time.monotonic(); " + f"result=project.run(ProjectCommand(('python','-c',{child_code!r}),root),timeout=1.0); " + "assert time.monotonic()-started < 3, 'project command exceeded bounded pipe drain'; " + "assert result.outcome=='exited' and result.exit_code==0; " + "assert 'parent-finished' in result.stdout" + ) + environment = dict( + os.environ, PYTHONPATH=str(Path(__file__).resolve().parents[3] / "packages/specfact-codebase/src") + ) + try: + subprocess.run( + [sys.executable, "-c", watchdog_code], + env=environment, + check=True, + capture_output=True, + text=True, + timeout=30, + ) + finally: + if descendant_pid.exists(): + with contextlib.suppress(ProcessLookupError): + os.kill(int(descendant_pid.read_text()), signal.SIGKILL) + + +def test_source_edits_allowed_but_dependency_drift_requires_reinspection(project, tmp_path): + prepared = ready(project, tmp_path) + cmd = ProjectCommand(("python", "-c", "print('ran')"), project[0]) + before = prepared.run(cmd, timeout=10) + (project[0] / "customer.py").write_text("raise AssertionError\n") + after = prepared.run(cmd, timeout=10) + assert after.outcome == "exited" and before.source_identity != after.source_identity + config = project[0] / "hatch.toml" + drift_command = ProjectCommand( + ( + "python", + "-c", + "import sys; from pathlib import Path; " + f"p=Path({str(config)!r}); p.write_text(p.read_text()+'\\n# during execution\\n'); " + "print('native-drift-stderr', file=sys.stderr)", + ), + project[0], + ) + drift = prepared.run(drift_command, timeout=10) + assert drift.outcome == "execution_failed" + assert "changed during execution" in drift.stderr + assert "native-drift-stderr" in drift.stderr + config.write_text(config.read_text() + "\n# changed\n") + stale = prepared.run(cmd, timeout=10) + assert stale.outcome == "execution_failed" + assert "reinspect" in stale.stderr.lower() + + +def test_selected_unsupported_context_rejected_before_native_query(project, tmp_path): + root, target, hatch, log = project + config = root / "hatch.toml" + config.write_text(config.read_text() + '\n[envs.review.env-vars]\nCUSTOM="{root}"\n') + plan = inspect_project( + root, ProjectSelection(environment="review", python=str(target / "bin/python"), hatch=str(hatch)) + ) + refused = prepare_project(plan, tmp_path / "invocation") + assert isinstance(refused, PreparationFailure) + assert "env-vars" in refused.reason + assert not log.exists() + + config.write_text("[envs.review]\n") + empty_plan = inspect_project( + root, ProjectSelection(environment="review", python=str(target / "bin/python"), hatch=str(hatch)) + ) + empty_result = prepare_project(empty_plan, tmp_path / "empty-invocation") + assert isinstance(empty_result, PreparationFailure) + assert "detached" in empty_result.reason + assert not log.exists() + + +def test_unrelated_scripts_and_locks_do_not_block_existing_execution(project, tmp_path): + root = project[0] + (root / "uv.lock").write_text("unrelated") + config = root / "hatch.toml" + config.write_text(config.read_text() + '\n[envs.other]\ntype="custom"\n[envs.review.scripts]\ncheck="python -V"\n') + assert ready(project, tmp_path).run(ProjectCommand(("python", "-V"), root), timeout=10).exit_code == 0 + + +@pytest.mark.parametrize("filename", ["pytest.toml", ".pytest.toml", ".pytest.ini", "tox.ini"]) +def test_pytest_config_drift_requires_reinspection(project, tmp_path, filename): + root = project[0] + config = root / filename + content = '[pytest]\ntestpaths=["tests"]\n' if filename.endswith("toml") else "[pytest]\ntestpaths=tests\n" + config.write_text(content) + prepared = ready(project, tmp_path) + command = ProjectCommand(("python", "-c", "print('customer-command')"), root) + before = prepared.run(command, timeout=10) + assert before.outcome == "exited" + config.write_text(content.replace("tests", "other_tests")) + stale = prepared.run(command, timeout=10) + assert stale.outcome == "execution_failed", stale + assert stale.stdout == "" + assert "reinspect" in stale.stderr.lower() + assert stale.source_identity != before.source_identity + refreshed = inspect_project(root, prepared.plan.selection) + assert refreshed.input_identity != prepared.plan.input_identity + + +def test_coverage_config_drift_requires_reinspection(project, tmp_path): + root = project[0] + config = root / ".coveragerc" + config.write_text("[run]\nomit=first/*\n") + prepared = ready(project, tmp_path) + command = ProjectCommand(("python", "-c", "print('customer-command')"), root) + before = prepared.run(command, timeout=10) + assert before.outcome == "exited" + config.write_text("[run]\nomit=second/*\n") + stale = prepared.run(command, timeout=10) + assert stale.outcome == "execution_failed", stale + assert stale.stdout == "" + assert "reinspect" in stale.stderr.lower() + assert stale.source_identity != before.source_identity + assert inspect_project(root, prepared.plan.selection).input_identity != prepared.plan.input_identity + + +def test_external_fresh_workspace_required(project, tmp_path): + root, target, hatch, _ = project + plan = inspect_project( + root, ProjectSelection(environment="review", python=str(target / "bin/python"), hatch=str(hatch)) + ) + assert isinstance(prepare_project(plan, root / "owned"), PreparationFailure) + workspace = tmp_path / "occupied" + workspace.mkdir() + (workspace / "unowned").write_text("preserve") + assert isinstance(prepare_project(plan, workspace), PreparationFailure) + assert (workspace / "unowned").read_text() == "preserve" + + +@pytest.mark.parametrize("location", ["inside", "symlink", "external"]) +def test_explicit_base_existing_target_excludes_workspace(project, tmp_path, location): + root, target, hatch, log = project + prefix = target + if location == "symlink": + prefix = tmp_path / "target-link" + prefix.symlink_to(target, target_is_directory=True) + workspace = (tmp_path if location == "external" else prefix) / "invocation" + before = {path.relative_to(target) for path in target.rglob("*")} + plan = inspect_project( + root, ProjectSelection(environment="review", python=str(Path(base_executable).resolve()), hatch=str(hatch)) + ) + result = prepare_project(plan, workspace) + if location == "external": + assert isinstance(result, PreparedProject), result + assert (workspace / "completed.json").is_file() + else: + assert isinstance(result, PreparationFailure), result + assert "outside the selected" in result.reason + assert not workspace.exists() + assert {path.relative_to(target) for path in target.rglob("*")} == before + assert "create" not in log.read_text() + + +def test_project_python_constraint_rejected_before_native_query(project, tmp_path): + root, target, hatch, log = project + (root / "pyproject.toml").write_text('[project]\nname="customer"\nversion="0"\nrequires-python=">=3.99"\n') + plan = inspect_project( + root, ProjectSelection(environment="review", python=str(target / "bin/python"), hatch=str(hatch)) + ) + result = prepare_project(plan, tmp_path / "invocation") + assert isinstance(result, PreparationFailure) + assert "requires-python" in result.reason + assert not log.exists() + + +def test_existing_environment_discovered_without_redundant_python_override(project, tmp_path, monkeypatch): + root, target, hatch, log = project + plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + prepared = prepare_project(plan, tmp_path / "invocation") + assert isinstance(prepared, PreparedProject), prepared + assert prepared.identity.interpreter.prefix == target + assert prepared.run(ProjectCommand(("python", "-V"), root), timeout=10).exit_code == 0 + assert "create" not in log.read_text() + + native_which = project_runtime.shutil.which + monkeypatch.setattr( + project_runtime.shutil, "which", lambda name: native_which(name) if name == str(hatch) else None + ) + (root / ".python-version").write_text(f"{sys.version_info.major}.{sys.version_info.minor}\n") + declared_plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + declared = prepare_project(declared_plan, tmp_path / "declared-invocation") + assert isinstance(declared, PreparedProject), declared + assert declared.identity.interpreter.prefix == target + (root / ".python-version").unlink() + config = root / "hatch.toml" + config.write_text(config.read_text() + f'python="{sys.version_info.major}.{sys.version_info.minor}"\n') + hatch_plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + configured = prepare_project(hatch_plan, tmp_path / "configured-invocation") + assert isinstance(configured, PreparedProject), configured + assert configured.identity.interpreter.prefix == target + + +@pytest.mark.parametrize( + "phase,failure", + [ + ("inspection", "enumeration"), + ("inspection", "classification"), + ("before-command", "enumeration"), + ("before-command", "classification"), + ("after-command", "enumeration"), + ("after-command", "classification"), + ], +) +def test_directory_traversal_failure_rejects_incomplete_identity(project, tmp_path, monkeypatch, phase, failure): + root, _, hatch, log = project + blocked = root / "blocked" + blocked.mkdir() + (blocked / "pytest.ini").write_text("[pytest]\ntestpaths=tests\n") + prepared = ready(project, tmp_path) + native_calls = log.read_text() + marker = tmp_path / "command-ran" + scandir = os.scandir + + def classify_or_deny(entry): + if ( + failure == "classification" + and Path(entry.path) == blocked + and (phase != "after-command" or marker.exists()) + ): + rejected = Mock(wraps=entry) + rejected.name = entry.name + rejected.path = entry.path + rejected.is_dir.side_effect = PermissionError("customer directory unreadable") + return rejected + return entry + + def scan_or_deny(path): + if failure == "enumeration" and Path(path) == blocked and (phase != "after-command" or marker.exists()): + raise PermissionError("customer directory unreadable") + entries = scandir(path) + iterator = MagicMock(wraps=entries) + iterator.__enter__.return_value = iterator + iterator.__exit__.side_effect = entries.__exit__ + iterator.__iter__.side_effect = lambda: iterator + iterator.__next__.side_effect = lambda: classify_or_deny(next(entries)) + return iterator + + monkeypatch.setattr(os, "scandir", scan_or_deny) + if phase == "inspection": + plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + assert "customer directory unreadable" in " ".join(plan.diagnostics) + assert not plan.input_identity + failed = prepare_project(plan, tmp_path / "new-invocation") + assert isinstance(failed, PreparationFailure) + assert "customer directory unreadable" in failed.reason + assert log.read_text() == native_calls + return + command = ProjectCommand( + ("python", "-c", f"from pathlib import Path; Path({str(marker)!r}).touch(); print('customer-output')"), root + ) + failed = prepared.run(command, timeout=10) + assert failed.outcome == "execution_failed", failed + assert "customer directory unreadable" in failed.stderr + assert marker.exists() == (phase == "after-command") + assert failed.stdout == ("customer-output\n" if phase == "after-command" else "") + + +def test_unreadable_inspection_identity_is_diagnostic(project, tmp_path, monkeypatch): + root, _, hatch, log = project + blocked = root / "requirements-extra.txt" + blocked.write_text("customer-dependency\n") + read_bytes = Path.read_bytes + + def read_or_deny(path): + if path == blocked: + raise PermissionError("customer identity unreadable") + return read_bytes(path) + + monkeypatch.setattr(Path, "read_bytes", read_or_deny) + plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + assert "customer identity unreadable" in " ".join(plan.diagnostics) + assert not plan.input_identity + failed = prepare_project(plan, tmp_path / "invocation") + assert isinstance(failed, PreparationFailure) + assert "customer identity unreadable" in failed.reason + assert not log.exists() + + +@pytest.mark.parametrize("after_execution", [False, True], ids=["before-command", "after-command"]) +def test_unreadable_execution_identity_is_structured(project, tmp_path, monkeypatch, after_execution): + prepared = ready(project, tmp_path) + root = project[0] + marker = tmp_path / "command-ran" + blocked = root / "hatch.toml" + read_bytes = Path.read_bytes + + def read_or_deny(path): + if path == blocked and (not after_execution or marker.exists()): + raise PermissionError("customer identity unreadable") + return read_bytes(path) + + monkeypatch.setattr(Path, "read_bytes", read_or_deny) + command = ProjectCommand( + ( + "python", + "-c", + f"import sys; from pathlib import Path; Path({str(marker)!r}).touch(); " + "print('command-output'); print('native-stderr', file=sys.stderr); " + "print('x'*65500, file=sys.stderr)", + ), + root, + ) + failed = prepared.run(command, timeout=10) + assert failed.outcome == "execution_failed" + assert failed.exit_code is None + assert "customer identity unreadable" in failed.stderr + assert marker.exists() is after_execution + if after_execution: + assert_captured_failure(failed) + else: + assert not failed.stdout + assert not failed.source_identity + + +def assert_captured_failure(failed): + assert failed.stdout == "command-output\n" + assert "native-stderr" in failed.stderr + assert len(failed.stderr) <= 65536 + assert failed.output_truncated + assert failed.source_identity + + +def test_cleanup_failure_preserves_preparation_failure(project, tmp_path, monkeypatch): + root, _, hatch, _ = project + plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + workspace = tmp_path / "invocation" + + def fail_completion(*args, **kwargs): + raise OSError("completion metadata cannot be written") + + def fail_cleanup(path): + assert path == workspace + raise PermissionError("workspace cleanup denied") + + monkeypatch.setattr(project_runtime.tempfile, "NamedTemporaryFile", fail_completion) + monkeypatch.setattr(project_runtime.shutil, "rmtree", fail_cleanup) + failed = prepare_project(plan, workspace) + assert isinstance(failed, PreparationFailure) + assert failed.reason == "completion metadata cannot be written" + assert "workspace cleanup denied" in failed.remedy + assert str(workspace) in failed.remedy + assert "fresh" in failed.remedy.lower() + assert workspace.is_dir() + assert not (workspace / "completed.json").exists() + + +def test_custom_hatch_config_is_diagnostic_before_native_query(project, tmp_path, monkeypatch): + root, _, hatch, log = project + custom = tmp_path / "global-hatch.toml" + custom.write_text('mode="project"\n') + monkeypatch.delenv("HATCH_CONFIG", raising=False) + earlier_plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + monkeypatch.setenv("HATCH_CONFIG", str(custom)) + changed_context = prepare_project(earlier_plan, tmp_path / "changed-context") + assert isinstance(changed_context, PreparationFailure) + assert "HATCH_CONFIG" in changed_context.reason + assert not log.exists() + plan = inspect_project(root, ProjectSelection(environment="review", hatch=str(hatch))) + assert "HATCH_CONFIG" in " ".join(plan.diagnostics) + failed = prepare_project(plan, tmp_path / "invocation") + assert isinstance(failed, PreparationFailure) + assert "HATCH_CONFIG" in failed.reason + assert not log.exists() + + +@pytest.mark.parametrize("mismatch", ["base-prefix", "version"]) +def test_absolute_hatch_python_must_match_selected_interpreter(project, tmp_path, monkeypatch, mismatch): + customer, _, hatch, _ = project + declared = Path(base_executable).resolve() + config = customer / "hatch.toml" + config.write_text(config.read_text() + f"python={json.dumps(str(declared))}\n") + plan = inspect_project(customer, ProjectSelection(environment="review", hatch=str(hatch))) + assert not plan.diagnostics + + def mismatched_declaration(executable, cwd): + facts = probe_python(executable, cwd) + if executable == declared: + return ( + replace(facts, base_prefix=tmp_path / "different-base") + if mismatch == "base-prefix" + else replace(facts, version=(3, 99, 0)) + ) + return facts + + monkeypatch.setattr(project_runtime, "_probe", mismatched_declaration) + failed = prepare_project(plan, tmp_path / "invocation") + assert isinstance(failed, PreparationFailure) + assert "declared Hatch Python" in failed.reason + assert "does not match" in failed.reason + + +def test_numeric_hatch_and_version_file_compare_whole_components(project): + customer, _, hatch, log = project + config = customer / "hatch.toml" + config.write_text(config.read_text() + 'python="3.11.1"\n') + (customer / ".python-version").write_text("3.11.15\n") + plan = inspect_project(customer, ProjectSelection(environment="review", hatch=str(hatch))) + assert "conflicts with .python-version" in " ".join(plan.diagnostics) + assert not log.exists() + config.write_text(config.read_text().replace('python="3.11.1"', 'python="3.11"')) + compatible = inspect_project(customer, ProjectSelection(environment="review", hatch=str(hatch))) + assert not compatible.diagnostics + assert not log.exists() + + +def test_cold_absolute_hatch_python_validates_created_base(project, tmp_path, monkeypatch): + customer, _, hatch, _ = project + declared = Path(base_executable).resolve() + config = customer / "hatch.toml" + config.write_text(config.read_text() + f"python={json.dumps(str(declared))}\n") + hatch.write_text( + f"#!{sys.executable}\nimport os,subprocess,sys\n" + "if sys.argv[1:]==['env','create','review']:\n" + f" subprocess.run([{str(declared)!r},'-m','venv','--without-pip'," + "os.environ['HATCH_ENV_TYPE_VIRTUAL_PATH']],check=True)\n" + "print('Hatch, version test' if sys.argv[1:]==['--version'] " + f"else {str(tmp_path / 'missing-native-target')!r})\n" + ) + plan = inspect_project(customer, ProjectSelection(environment="review", hatch=str(hatch))) + assert not plan.diagnostics + workspace = tmp_path / "invocation" + target_python = workspace / "environment/bin/python" + + def mismatched_created_target(executable, cwd): + facts = probe_python(executable, cwd) + if executable == target_python: + return replace(facts, base_prefix=tmp_path / "different-created-base") + return facts + + monkeypatch.setattr(project_runtime, "_probe", mismatched_created_target) + failed = prepare_project(plan, workspace) + assert isinstance(failed, PreparationFailure) + assert "declared Hatch Python" in failed.reason + assert "does not match" in failed.reason + assert not (workspace / "completed.json").exists() + + +@pytest.mark.parametrize("existing", [True, False], ids=["existing", "cold"]) +def test_explicit_base_python_must_match_prepared_target(project, tmp_path, monkeypatch, existing): + customer, target, hatch, _ = project + selected = Path(base_executable).resolve() + workspace = tmp_path / "invocation" + if not existing: + hatch.write_text( + f"#!{sys.executable}\nimport os,subprocess,sys\n" + "if sys.argv[1:]==['env','create','review']:\n" + f" subprocess.run([{str(selected)!r},'-m','venv','--without-pip'," + "os.environ['HATCH_ENV_TYPE_VIRTUAL_PATH']],check=True)\n" + "print('Hatch, version test' if sys.argv[1:]==['--version'] " + f"else {str(tmp_path / 'missing-native-target')!r})\n" + ) + target = workspace / "environment" + other_base = tmp_path / "different-python-installation" + + def different_target_base(executable, cwd): + facts = probe_python(executable, cwd) + return replace(facts, base_prefix=other_base) if executable == target / "bin/python" else facts + + monkeypatch.setattr(project_runtime, "_probe", different_target_base) + plan = inspect_project(customer, ProjectSelection(environment="review", python=str(selected), hatch=str(hatch))) + assert not plan.diagnostics + failed = prepare_project(plan, workspace) + assert isinstance(failed, PreparationFailure), failed + assert "selected Python" in failed.reason + assert "base" in failed.reason + assert str(other_base) in failed.reason + assert str(Path(sys.base_prefix).resolve()) in failed.reason + assert not (workspace / "completed.json").exists() + + +@pytest.mark.parametrize("mismatch", [False, True], ids=["matching", "wrong-patch"]) +def test_explicit_numeric_python_checks_all_requested_components(project, tmp_path, monkeypatch, mismatch): + customer, target, hatch, log = project + actual = tuple(sys.version_info[:3]) + requested = (*actual[:2], actual[2] + 1000) if mismatch else actual + request = ".".join(map(str, requested)) + lookup = "python" + ".".join(map(str, actual[:2])) + native_which = project_runtime.shutil.which + monkeypatch.setattr( + project_runtime.shutil, + "which", + lambda name: str(target / "bin/python") if name == lookup else native_which(name), + ) + plan = inspect_project(customer, ProjectSelection(environment="review", python=request, hatch=str(hatch))) + prepared = prepare_project(plan, tmp_path / "invocation") + if mismatch: + assert isinstance(prepared, PreparationFailure), prepared + assert request in prepared.reason + assert str(actual) in prepared.reason + assert not log.exists() + else: + assert isinstance(prepared, PreparedProject), prepared + assert prepared.identity.interpreter.version == actual + assert "create" not in log.read_text() + + +def test_controller_python_startup_removed_and_explicit_override_honored(project, tmp_path, monkeypatch): + prepared = ready(project, tmp_path) + startup = { + "PYTHONOPTIMIZE": "1", + "PYTHONSTARTUP": "/controller/startup.py", + "PYTHONUSERBASE": "/controller/user-base", + "PYTHONSAFEPATH": "1", + "PYTHONEXECUTABLE": "/controller/python", + "PYTHONINSPECT": "1", + } + for name, value in startup.items(): + monkeypatch.setenv(name, value) + code = ( + "import os,sys,json\n" + f"names={list(startup)!r}\n" + "print(json.dumps([sys.flags.optimize,*[os.getenv(name) for name in names]]))" + ) + inherited = prepared.run(ProjectCommand(("python", "-c", code), project[0]), timeout=10) + assert inherited.outcome == "exited" and inherited.exit_code == 0, inherited + assert json.loads(inherited.stdout) == [0, *[None] * len(startup)] + explicit = prepared.run(ProjectCommand(("python", "-c", code), project[0], {"PYTHONOPTIMIZE": "1"}), timeout=10) + assert explicit.outcome == "exited" and explicit.exit_code == 0, explicit + assert json.loads(explicit.stdout) == [1, "1", *[None] * (len(startup) - 1)] diff --git a/tests/unit/test_capsule_analyzer_diagnostics.py b/tests/unit/test_capsule_analyzer_diagnostics.py new file mode 100644 index 00000000..80b6fc6b --- /dev/null +++ b/tests/unit/test_capsule_analyzer_diagnostics.py @@ -0,0 +1,358 @@ +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] +WORKFLOW = REPO_ROOT / ".github/workflows/capsule-customer-execution.yml" +JOBS = ("customer", "independent-review") +SOURCE = "tests/test_customer.py" +PRIVATE = "PRIVATE_SECRET_/customer/dependency.py" +NATIVE_DETAILS = ( + "Set SPECFACT_HATCH_PROOF_WORKSPACE with native Hatch and Python 3.11 prerequisites for the acceptance cell", + "Native Hatch and project Python are required for customer storage acceptance", + "Native Hatch and project Python are required for absolute declaration acceptance", +) +CROSSHAIR_MARKERS = ( + ("CrossHair timed out before mandatory evidence completed.", "crosshair_timeout_observed"), + ("CrossHair process error:", "crosshair_process_error_observed"), + ("Unrecognized CrossHair output:", "crosshair_unrecognized_output_observed"), + ("Unable to execute CrossHair:", "crosshair_execute_error_observed"), +) +EXCEPTIONS = ( + ("ModuleNotFoundError", "module_not_found_observed"), + ("ImportError", "import_error_observed"), + ("SyntaxError", "syntax_error_observed"), + ("PermissionError", "permission_error_observed"), + ("TypeError", "type_error_observed"), + ("FileNotFoundError", "file_not_found_observed"), + ("ValueError", "value_error_observed"), + ("wrong parameter order", "wrong_parameter_order_observed"), +) + + +@pytest.fixture(autouse=True) +def record_selector(request, record_property): + record_property("specfact.selector", request.node.nodeid) + + +def _recipe(job, projection): + steps = yaml.safe_load(WORKFLOW.read_text())["jobs"][job]["steps"] + if projection == "failure" and job == "independent-review": + return next(step["run"] for step in steps if step.get("name") == "Project independent review failure privately") + identifier = "deferred_review" if job == "customer" else "independent_review" + return next(step["run"] for step in steps if step.get("id") == identifier) + + +def _program(job, projection): + recipe = _recipe(job, projection) + marker = "PY_CANDIDATE_FAILURE" if job == "customer" else "PY_REVIEW_FAILURE" + if projection == "locations": + invocation = next(line for line in recipe.splitlines() if "REVIEW_PUBLIC_EXIT=" in line and "<<'PY'" in line) + return recipe.split(invocation + "\n", 1)[1].split("\nPY", 1)[0] + return recipe.split("<<'" + marker + "'", 1)[1].split("\n", 1)[1].split("\n" + marker, 1)[0] + + +@pytest.fixture(name="project") +def customer_project(tmp_path): + source = tmp_path / SOURCE + source.parent.mkdir() + source.write_text("def test_customer():\n assert True\n") + for arguments in (("init", "-q"), ("add", SOURCE)): + subprocess.run(["git", *arguments], cwd=tmp_path, capture_output=True, check=True) + (tmp_path / ".specfact").mkdir() + return tmp_path + + +def _run(project, job, projection, data, review_exit=17): + report = project / ".specfact/code-review.json" + payload = data if isinstance(data, str) else json.dumps(data) + report.write_text(payload) + environment = dict( + os.environ, + REVIEW_REPORT=str(report), + REVIEW_PUBLIC_REPORT=str(report), + REVIEW_LOG=str(project / "absent.log"), + REVIEW_PUBLIC_EXIT=str(review_exit), + ) + result = subprocess.run( + [sys.executable, "-c", _program(job, projection)], + cwd=project, + env=environment, + capture_output=True, + text=True, + check=False, + ) + assert PRIVATE not in result.stdout + result.stderr + assert report.read_text() == payload + return result + + +def _report(findings=(), rows=(), oversized=False): + return { + "findings": list(findings), + "analyzer_evidence": list(rows), + "assurance_status": "FAIL", + "overall_verdict": "FAIL", + "private_padding": PRIVATE + ("x" * (2 * 1024 * 1024) if oversized else ""), + } + + +def _finding(**fields): + return { + "file": SOURCE, + "line": 1, + "severity": "warning", + "tool": "pytest", + "category": "tool_error", + "rule": "tool_error", + "message": PRIVATE, + **fields, + } + + +def _output(result): + assert result.returncode == 0, result.stderr + return [json.loads(line) for line in result.stdout.splitlines()] + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize("oversized", [False, True]) +@pytest.mark.parametrize("side", ["base", "head", "unavailable"]) +def test_failure_provenance_retains_actual_side(project, job, oversized, side): + evidence = {"execution_state": "error", "evidence_outcome": "UNKNOWN", "diagnostic": PRIVATE} + row = {"id": "contracts", **(evidence if side == "unavailable" else {side: evidence})} + result = _output(_run(project, job, "failure", _report(rows=[row], oversized=oversized))) + observed = ( + next(item["incomplete_snapshot_sides"] for item in result if "incomplete_snapshot_sides" in item) + if job == "customer" + else result[0].get("error_analyzer_sides", {}).get("contracts") + ) + assert observed == [side] + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize("oversized", [False, True]) +@pytest.mark.parametrize("marker,code", CROSSHAIR_MARKERS, ids=["timeout", "process", "unrecognized", "execute"]) +def test_crosshair_classes_match_structured_and_oversized(project, job, oversized, marker, code): + finding = _finding(tool="crosshair", message=marker + " " + PRIVATE) + rows = _output(_run(project, job, "failure", _report([finding], oversized=oversized))) + if job == "customer": + observations = [ + row["diagnostic_class"] for row in rows if row.get("diagnostic_class") != "candidate_report_unavailable" + ] + else: + observations = rows[0].get("crosshair_failure_observations") + assert observations == [code] + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize( + "exception,code", + EXCEPTIONS, + ids=["module", "import", "syntax", "permission", "type", "file", "value", "parameter-order"], +) +def test_crosshair_exception_observations_are_closed(project, job, exception, code): + finding = _finding(tool="crosshair", message="CrossHair process error: " + exception + ": " + PRIVATE) + rows = _output(_run(project, job, "failure", _report([finding]))) + if job == "customer": + assert rows == [ + { + "analyzer": "contracts", + "diagnostic_class": "crosshair_process_error_observed", + "exception_observations": [code], + } + ] + else: + assert rows[0].get("crosshair_failure_observations") == sorted(["crosshair_process_error_observed", code]) + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize( + "state,status,blocking", + [ + ("fixed", "fixed", False), + ("introduced", "open", True), + ("unchanged", "open", True), + ("unknown", "waived-by-reference", False), + ], +) +def test_finding_lifecycle_preserves_authoritative_fields(project, job, state, status, blocking): + finding = _finding(differential_state=state, status=status, blocking=blocking) + rows = _output(_run(project, job, "locations", _report([finding]))) + location = rows[0]["finding_location"] + assert {field: location.get(field) for field in ("differential_state", "status", "blocking")} == { + "differential_state": state, + "status": status, + "blocking": blocking, + } + assert location["severity"] == "warning" + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize("value", [None, 0, 1, "false", [], {}]) +def test_invalid_lifecycle_never_defaults_to_nonblocking(project, job, value): + finding = _finding(differential_state=value, status=value, blocking=value) + rows = _output(_run(project, job, "locations", _report([finding]))) + assert not {"differential_state", "status", "blocking"}.intersection(rows[0]["finding_location"]) + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize( + "message,code", + [ + (PRIVATE, "unclassified"), + ("project_pytest_collection_failed:" + PRIVATE, "project_pytest_collection_failed"), + ("[Errno 2] " + PRIVATE, "file_missing"), + ("[Errno 13] " + PRIVATE, "permission_denied"), + ], + ids=["unknown", "collection", "file-missing", "permission"], +) +def test_pytest_error_classification_does_not_infer_from_anchor(project, job, message, code): + rows = _output(_run(project, job, "locations", _report([_finding(message=message)]))) + location = rows[0]["finding_location"] + assert location.get("diagnostic_code") == code + assert location["file"] == SOURCE + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize( + "detail", [*NATIVE_DETAILS, PRIVATE, ""], ids=["workspace", "storage", "declaration", "unknown", "absent"] +) +@pytest.mark.parametrize("outcome", ["skipped", "failed"]) +def test_native_prerequisite_classification_uses_only_recorded_skips(project, job, detail, outcome): + nodeid = SOURCE + "::test_customer" + record = {"nodeid": nodeid, "outcome": outcome, "phase": "call", "detail": detail} + row = {"id": "targeted-pytest-coverage", "head": {"target_execution": {"records": [record]}}} + finding = _finding( + category="testing", rule="TEST_OUTCOME_NOT_PASS", message=f"Test {nodeid} {outcome} during call." + ) + rows = _output(_run(project, job, "locations", _report([finding], [row]))) + location = rows[0]["finding_location"] + expected = "native_prerequisites_not_supplied" if detail in NATIVE_DETAILS and outcome == "skipped" else None + assert location.get("test_diagnostic") == expected + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize( + "payload", + ["{" + PRIVATE, '{"findings":[],"findings":[],"analyzer_evidence":[]}', "[]", "x" * (32 * 1024 * 1024 + 1)], + ids=["invalid-json", "duplicate-key", "invalid-shape", "over-bound"], +) +def test_failure_projection_rejects_invalid_private_reports(project, job, payload): + rows = _output(_run(project, job, "failure", payload)) + assert all(not row.get("error_analyzers") and not row.get("incomplete_snapshot_sides") for row in rows) + assert all(row.get("overall_verdict") != "PASS" for row in rows) + + +@pytest.mark.parametrize("job", JOBS) +def test_public_location_bounds_and_private_fields_remain_closed(project, job): + findings = [_finding(file="/" + PRIVATE), _finding(file="../" + PRIVATE), _finding(line=True)] + findings += [ + _finding(line=index + 1, differential_state="fixed", status="fixed", blocking=False) for index in range(205) + ] + rows = _output(_run(project, job, "locations", _report(findings))) + locations = [row["finding_location"] for row in rows] + assert len(locations) == 200 + assert [location["line"] for location in locations] == list(range(1, 201)) + assert all( + set(location) + == { + "file", + "line", + "severity", + "category", + "tool", + "rule", + "differential_state", + "status", + "blocking", + "diagnostic_code", + } + for location in locations + ) + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize("review_exit", [17, 124]) +def test_diagnostics_preserve_failed_review_capture_and_identity(project, job, review_exit): + recipe = _recipe(job, "locations") + assert recipe.rstrip().endswith('exit "$review_exit"') + assert "review_exit=$?" in recipe + assert 'REVIEW_PUBLIC_EXIT="$review_exit"' in recipe + data = _report([_finding(differential_state="fixed", status="fixed", blocking=False)]) + data.update(subject_commit=PRIVATE, reviewer_identity=PRIVATE) + rows = _output(_run(project, job, "locations", data, review_exit)) + assert all(row.get("status") != "PASS" and row.get("overall_verdict") != "PASS" for row in rows) + if review_exit == 124: + assert rows[0]["diagnostic"] == "analysis_timeout" + + +@pytest.mark.parametrize("job", JOBS) +def test_failure_iteration_limits_do_not_reveal_late_private_evidence(project, job): + rows = [{"id": "ruff"}] * 10 + [{"id": "contracts", "base": {"execution_state": "error"}}] + findings = [{}] * 10_000 + [_finding(tool="crosshair", message="CrossHair process error: " + PRIVATE)] + output = _output(_run(project, job, "failure", _report(findings, rows))) + if job == "customer": + assert output == [] + else: + assert output[0].get("error_analyzer_sides", {}) == {} + assert output[0].get("crosshair_failure_observations", []) == [] + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize( + "detail", ["E ModuleNotFoundError: " + PRIVATE, "E PrivateException: " + PRIVATE], ids=["known", "unknown"] +) +def test_recorded_pytest_exceptions_remain_allowlisted(project, job, detail): + nodeid = SOURCE + "::test_customer" + record = {"nodeid": nodeid, "outcome": "failed", "phase": "call", "detail": detail} + evidence = {"id": "targeted-pytest-coverage", "head": {"target_execution": {"records": [record]}}} + finding = _finding(category="testing", rule="TEST_OUTCOME_NOT_PASS", message=f"Test {nodeid} failed during call.") + rows = _output(_run(project, job, "locations", _report([finding], [evidence]))) + expected = "ModuleNotFoundError" if detail.startswith("E ModuleNotFoundError:") else None + assert rows[0]["finding_location"].get("test_failure_class") == expected + + +@pytest.mark.parametrize("job", JOBS) +@pytest.mark.parametrize( + "message,failure_class,diagnostic_code", + [ + ("Unable to execute targeted tests: [Errno 2] " + PRIVATE, "pytest_execute_error", "file_missing"), + ("Unable to execute targeted tests: [Errno 13] " + PRIVATE, "pytest_execute_error", "permission_denied"), + ("Unable to execute targeted tests: " + PRIVATE, "pytest_execute_error", "unclassified"), + ( + "Unable to read targeted pytest evidence: project_pytest_collection_failed:" + PRIVATE, + "pytest_evidence_read_error", + "project_pytest_collection_failed", + ), + ("Unable to read targeted pytest evidence: " + PRIVATE, "pytest_evidence_read_error", "unclassified"), + ( + "Targeted pytest observer, JUnit, and process outcomes could not be reconciled.", + "pytest_outcome_reconciliation_failed", + "unclassified", + ), + ("private wrapper: Unable to execute targeted tests: [Errno 2] " + PRIVATE, None, "unclassified"), + ], + ids=[ + "execute-file", + "execute-permission", + "execute-unknown", + "read-collection", + "read-unknown", + "reconciliation", + "arbitrary-wrapper", + ], +) +def test_pytest_producer_failures_retain_fixed_cause_without_private_text( + project, job, message, failure_class, diagnostic_code +): + rows = _output(_run(project, job, "locations", _report([_finding(message=message)]))) + location = rows[0]["finding_location"] + assert location.get("failure_class") == failure_class + assert location.get("diagnostic_code") == diagnostic_code diff --git a/tests/unit/test_capsule_review_projection.py b/tests/unit/test_capsule_review_projection.py index 113abd30..7585292c 100644 --- a/tests/unit/test_capsule_review_projection.py +++ b/tests/unit/test_capsule_review_projection.py @@ -283,7 +283,7 @@ def _write_independent_failure_fixture(tmp_path, case): return path, log -def _expected_review_projection(expected): +def _expected_review_projection(expected, case): outcome, flags, codes = expected verdict = "FAIL" if outcome == "structured_report" else "UNAVAILABLE" keys = ("findings_present", "tool_error_present", "execution_error_present", "unknown_evidence_present") @@ -301,6 +301,10 @@ def _expected_review_projection(expected): projection["report_header_observed"] = False projection["error_analyzer_sides"] = {} projection["crosshair_failure_observations"] = [] + if outcome == "structured_report": + sides = {"nested": {"pylint": ["head"]}, "direct": {"pylint": ["unavailable"]}} + projection["error_analyzer_sides"] = sides.get(case, {}) + projection["crosshair_failure_observations"] = [] return projection @@ -331,7 +335,7 @@ def test_independent_review_failure_keeps_nested_evidence_and_private_tokens(tmp monkeypatch.setenv("REVIEW_LOG", str(log)) result = _run_projector(tmp_path, program) assert result.returncode == 0 - assert json.loads(result.stdout) == _expected_review_projection(expected) + assert json.loads(result.stdout) == _expected_review_projection(expected, case) assert "private-token" not in result.stdout + result.stderr @@ -1100,9 +1104,9 @@ def _depth_failure(value, *args, **kwargs): ), ("[Errno 2] PRIVATE_PATH", "file_missing"), ("[Errno 13] PRIVATE_PATH", "permission_denied"), - ("PRIVATE project_pytest_execution_incomplete", None), - ("project_pytest_execution_incomplete_PRIVATE", None), - ("PRIVATE_UNKNOWN", None), + ("PRIVATE project_pytest_execution_incomplete", "unclassified"), + ("project_pytest_execution_incomplete_PRIVATE", "unclassified"), + ("PRIVATE_UNKNOWN", "unclassified"), ], ) def test_public_pytest_failure_codes_withhold_private_payload(tmp_path: Path, job_name, message, code): diff --git a/tests/unit/test_capsule_customer_gate.py b/tests/unit/test_customer_review_ci.py similarity index 96% rename from tests/unit/test_capsule_customer_gate.py rename to tests/unit/test_customer_review_ci.py index c55dfbc2..12b6b059 100644 --- a/tests/unit/test_capsule_customer_gate.py +++ b/tests/unit/test_customer_review_ci.py @@ -486,39 +486,41 @@ def _orchestrator_workflow(): ) -def test_required_quality_runs_capsule_assertion_even_after_dependency_failure(): +def test_required_quality_runs_native_assertion_even_after_dependency_failure(): workflow = _orchestrator_workflow() quality = workflow["jobs"]["quality"] - assert "customer-capsules" in quality["needs"] + assert "native-review" in quality["needs"] assert "always()" in quality["if"] - assert quality["steps"][0]["name"] == "Require signature and customer capsule prerequisites" - assert workflow["jobs"]["customer-capsules"]["uses"] == "./.github/workflows/capsule-customer-execution.yml" + assert quality["steps"][0]["name"] == "Require signature prerequisite" + assert workflow["jobs"]["native-review"]["uses"] == "./.github/workflows/native-pr-review.yml" @pytest.mark.parametrize( - "required,result,signature,expected", + "legacy_required,result,signature,expected", [ ("true", "success", "success", 0), ("true", "failure", "success", 1), ("true", "skipped", "success", 1), ("true", "cancelled", "success", 1), - ("false", "skipped", "success", 0), + ("false", "skipped", "success", 1), ("false", "skipped", "failure", 1), ], ) -def test_quality_prerequisite_step_cannot_turn_capsule_failure_into_success(required, result, signature, expected): +def test_quality_prerequisite_cannot_waive_native_review(legacy_required, result, signature, expected): import subprocess - step = _orchestrator_workflow()["jobs"]["quality"]["steps"][0] - assert step["name"] == "Require signature and customer capsule prerequisites" - result = subprocess.run( - ["/bin/bash", "-c", step["run"]], + steps = _orchestrator_workflow()["jobs"]["quality"]["steps"] + signature_step = steps[0] + native_step = next(step for step in steps if step.get("name") == "Require independent native review") + assert signature_step["name"] == "Require signature prerequisite" + completed = subprocess.run( + ["/bin/bash", "-c", signature_step["run"] + "\n" + native_step["run"]], capture_output=True, text=True, check=False, - env={"CAPSULE_REQUIRED": required, "CAPSULE_RESULT": result, "SIGNATURE_RESULT": signature}, + env={"CAPSULE_REQUIRED": legacy_required, "NATIVE_REVIEW_RESULT": result, "SIGNATURE_RESULT": signature}, ) - assert result.returncode == expected + assert completed.returncode == expected def test_minimum_core_smoke_uses_current_authenticated_lock_not_completed_checkpoint(): diff --git a/tests/unit/workflows/test_native_hatch_acceptance.py b/tests/unit/workflows/test_native_hatch_acceptance.py new file mode 100644 index 00000000..6825c020 --- /dev/null +++ b/tests/unit/workflows/test_native_hatch_acceptance.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +import pytest +import yaml + + +ROOT = Path(__file__).resolve().parents[3] +MAPPING = "openspec/changes/code-review-17-project-runtime-bootstrap/requirements-evidence.yaml" +SELECTORS = ["tests/example.py::test_first", "tests/example.py::test_second"] + + +@pytest.fixture(autouse=True) +def record_selector(request, record_property): + record_property("specfact.selector", request.node.nodeid) + + +def native_job(): + path = ROOT / ".github/workflows/native-hatch-acceptance.yml" + assert path.is_file(), "Mandatory native Hatch acceptance workflow is missing" + workflow = yaml.safe_load(path.read_text()) + return workflow, workflow["jobs"]["native-hatch"] + + +def steps_using(job, action): + return [step for step in job["steps"] if step.get("uses", "").startswith(action + "@")] + + +def step_named(job, name): + return next(step for step in job["steps"] if step.get("name") == name) + + +def test_native_hatch_job_binds_exact_head_and_separate_interpreters(): + workflow, job = native_job() + assert workflow["permissions"] == {"contents": "read"} + assert not job.get("needs") + assert not job.get("continue-on-error", False) + assert_native_checkout(job) + assert_native_evidence_upload(job) + setups = {s["id"]: s["with"]["python-version"] for s in steps_using(job, "actions/setup-python")} + assert setups == {"project-python": "3.11.15", "controller-python": "3.12.13"} + assert any("hatch==1.18.1" in s.get("run", "") for s in job["steps"]) + step = step_named(job, "Run mapped native Hatch acceptance") + assert_native_inputs(step) + + +def assert_native_checkout(job): + checkout = steps_using(job, "actions/checkout")[0] + assert checkout["with"] == { + "ref": "${{ github.event.pull_request.head.sha || github.sha }}", + "fetch-depth": 0, + "persist-credentials": False, + } + + +def assert_native_inputs(step): + assert step["env"]["SPECFACT_HATCH_PROJECT_PYTHON"] == "${{ steps.project-python.outputs.python-path }}" + assert "SPECFACT_HATCH_EXECUTABLE" in step["env"] + assert "SPECFACT_HATCH_PROOF_WORKSPACE" in step["env"] + + +def assert_native_evidence_upload(job): + artifacts = steps_using(job, "actions/upload-artifact") + assert len(artifacts) == 1 + assert artifacts[0]["if"] == "always()" + assert "receipt.json" in artifacts[0]["with"]["path"] + assert "junit.xml" in artifacts[0]["with"]["path"] + + +@pytest.mark.parametrize("result,expected", [("success", 0), ("failure", 1), ("cancelled", 1), ("skipped", 1)]) +def test_native_hatch_result_blocks_quality_without_replacing_security(result, expected): + workflow = yaml.safe_load((ROOT / ".github/workflows/pr-orchestrator.yml").read_text()) + assert "native-hatch" in workflow["jobs"], "Required native Hatch prerequisite is missing" + assert workflow["jobs"]["native-hatch"]["uses"] == "./.github/workflows/native-hatch-acceptance.yml" + quality = workflow["jobs"]["quality"] + assert {"native-hatch", "native-scope", "native-review", "verify-module-signatures"} <= set(quality["needs"]) + step = step_named(quality, "Require native Hatch acceptance") + assert step["env"]["NATIVE_HATCH_RESULT"] == "${{ needs.native-hatch.result }}" + completed = subprocess.run( + ["bash", "-c", step["run"]], env={"NATIVE_HATCH_RESULT": result}, capture_output=True, text=True, check=False + ) + assert completed.returncode == expected, completed.stdout + completed.stderr + + +def junit(selectors, outcome): + suite = ET.Element("testsuite") + for index, selector in enumerate(selectors): + case = ET.SubElement(suite, "testcase", name=f"case{index}") + properties = ET.SubElement(case, "properties") + ET.SubElement(properties, "property", name="specfact.selector", value=selector) + if index == 0 and outcome in {"skipped", "failure", "error"}: + ET.SubElement(case, outcome) + return ET.tostring(suite, encoding="unicode") + + +@pytest.mark.parametrize( + "outcome", + [ + "complete", + "skipped", + "failure", + "error", + "missing", + "duplicate", + "extra", + "nonzero", + "no-report", + "empty-mapping", + "duplicate-mapping", + "archived-mapping", + "ambiguous-archive", + ], +) +def test_native_hatch_accepts_only_complete_exact_mapping(tmp_path, outcome): + _, job = native_job() + step = step_named(job, "Run mapped native Hatch acceptance") + program = step["run"] + prefix = "hatch run python - <<'PY'\n" + assert program.startswith(prefix) and program.rstrip().endswith("\nPY") + program = program[len(prefix) :].rstrip()[:-3] + write_mapping(tmp_path, outcome) + observed = { + "missing": SELECTORS[:1], + "duplicate": SELECTORS + SELECTORS[:1], + "extra": [*SELECTORS, "tests/example.py::test_extra"], + }.get(outcome, SELECTORS) + source = tmp_path / "source.xml" + source.write_text(junit(observed, outcome)) + fake = tmp_path / "pytest.py" + fake.write_text( + "import json,os,shutil,sys\nfrom pathlib import Path\n" + "Path('pytest-argv.json').write_text(json.dumps(sys.argv[1:]))\n" + "if os.environ['OUTCOME'] != 'no-report':\n" + " target=Path(sys.argv[sys.argv.index('--junitxml')+1]); target.parent.mkdir(parents=True,exist_ok=True)\n" + " shutil.copyfile(os.environ['SOURCE_XML'],target)\n" + "raise SystemExit(7 if os.environ['OUTCOME']=='nonzero' else 0)\n" + ) + report = tmp_path / "out/junit.xml" + completed = subprocess.run( + [sys.executable, "-c", program], + cwd=tmp_path, + env=os.environ + | { + "PYTHONPATH": str(tmp_path), + "OUTCOME": outcome, + "SOURCE_XML": str(source), + "NATIVE_HATCH_JUNIT": str(report), + }, + capture_output=True, + text=True, + check=False, + ) + assert (completed.returncode == 0) == (outcome in {"complete", "archived-mapping"}), ( + completed.stdout + completed.stderr + ) + if outcome == "ambiguous-archive": + assert "exactly one" in completed.stdout + completed.stderr + calls = tmp_path / "pytest-argv.json" + if outcome in {"empty-mapping", "duplicate-mapping", "ambiguous-archive"}: + assert not calls.exists() + else: + arguments = json.loads(calls.read_text()) + assert [s for s in arguments if "::" in s] == SELECTORS + + +def write_mapping(tmp_path, outcome): + mapping = tmp_path / MAPPING + mapping.parent.mkdir(parents=True) + selected = {"empty-mapping": [], "duplicate-mapping": SELECTORS + SELECTORS[:1]}.get(outcome, SELECTORS) + mapping.write_text( + json.dumps( + { + "schema_version": "2", + "requirements": { + "native": { + "verification_cases": [ + {"method": "test", "selector": {"runner": "pytest", "node_id": node}} for node in selected + ] + + [{"method": "inspection"}] + } + }, + } + ) + ) + if outcome in {"archived-mapping", "ambiguous-archive"}: + archive = tmp_path / "openspec/changes/archive/2026-01-01-code-review-17-project-runtime-bootstrap" + archive.mkdir(parents=True) + archived = archive / mapping.name + mapping.rename(archived) + if outcome == "ambiguous-archive": + other = archive.with_name("2026-01-02-code-review-17-project-runtime-bootstrap") + other.mkdir() + (other / mapping.name).write_bytes(archived.read_bytes())