diff --git a/ci/conditional-test-policy.json b/ci/conditional-test-policy.json index 8616da5..2c2ae96 100644 --- a/ci/conditional-test-policy.json +++ b/ci/conditional-test-policy.json @@ -14,6 +14,48 @@ "timeout_minutes": 20 } }, + "environment_waivers": [ + { + "gate": "cuda-runtime", + "reason": "No dedicated CUDA runner is confirmed; the enable variable is unset and the current maintainer token cannot enumerate repository runners.", + "issue": "https://github.com/elder-plinius/OBLITERATUS/issues/110", + "opened": "2026-08-16", + "expires": "2026-09-15", + "blocked_claim": "CUDA runtime support, compatibility, and performance are not claimed while this waiver is active." + }, + { + "gate": "bitsandbytes-runtime", + "reason": "No dedicated CUDA and bitsandbytes runner is confirmed; the enable variable is unset and the current maintainer token cannot enumerate repository runners.", + "issue": "https://github.com/elder-plinius/OBLITERATUS/issues/110", + "opened": "2026-08-16", + "expires": "2026-09-15", + "blocked_claim": "bitsandbytes runtime support, compatibility, and performance are not claimed while this waiver is active." + }, + { + "gate": "mps-runtime", + "reason": "No Apple Silicon MPS runner is confirmed; the enable variable is unset and the current maintainer token cannot enumerate repository runners.", + "issue": "https://github.com/elder-plinius/OBLITERATUS/issues/110", + "opened": "2026-08-16", + "expires": "2026-09-15", + "blocked_claim": "Apple MPS runtime support, compatibility, and performance are not claimed while this waiver is active." + }, + { + "gate": "mlx-runtime", + "reason": "No Apple Silicon MLX runner is confirmed; the enable variable is unset and the current maintainer token cannot enumerate repository runners.", + "issue": "https://github.com/elder-plinius/OBLITERATUS/issues/110", + "opened": "2026-08-16", + "expires": "2026-09-15", + "blocked_claim": "Apple MLX runtime support, compatibility, and performance are not claimed while this waiver is active." + }, + { + "gate": "remote-execution", + "reason": "No least-privileged remote test target or required repository credentials are configured.", + "issue": "https://github.com/elder-plinius/OBLITERATUS/issues/110", + "opened": "2026-08-16", + "expires": "2026-09-15", + "blocked_claim": "Remote execution support, compatibility, and performance are not claimed while this waiver is active." + } + ], "gates": [ { "id": "model-download-runtime", diff --git a/docs/conditional-testing.md b/docs/conditional-testing.md index d9ba3af..1d5358c 100644 --- a/docs/conditional-testing.md +++ b/docs/conditional-testing.md @@ -12,6 +12,13 @@ days, and treats evidence older than eight days as stale. Every workflow run pub a summary showing selected, successful, failed, and not-selected/no-fresh-evidence gates. +Unavailable hardware or credential-bound gates may use a committed environment +waiver for at most 30 days. Each waiver names one gate, a canonical tracking issue, +the reason, its opening and expiry dates, and the support claim it blocks. Invalid, +duplicate, future-dated, overlong, or expired waivers fail the policy job. The current +waivers are tracked by [issue #110](https://github.com/elder-plinius/OBLITERATUS/issues/110) +and expire on 2026-09-15; they are not evidence that any waived backend works. + ## Hosted gates The model gate downloads only @@ -99,4 +106,8 @@ hosted runner-minutes plus any provider charge. failure, error, or skip. A selected workflow job therefore cannot become green through an availability skip or unconditional success conversion. The final summary also fails if any selected job is not successful. Unselected jobs are explicitly reported -as `not_selected_no_fresh_evidence`; they are not evidence of backend support. +as `not_selected_no_fresh_evidence`; they are not evidence of backend support. An +unselected hardware or credential-bound gate with a valid waiver is reported as +`waived_no_support_claim`, including its tracker, expiry, and blocked claim. Selecting +and successfully running that gate produces `success` instead of relying on the +waiver. diff --git a/scripts/check_conditional_policy.py b/scripts/check_conditional_policy.py index ea367bf..1f23806 100644 --- a/scripts/check_conditional_policy.py +++ b/scripts/check_conditional_policy.py @@ -13,6 +13,9 @@ from pathlib import Path REQUIRED_GATE_FIELDS = { "id", "job", "marker", "runner", "prerequisites", "expected_cost", "coverage_paths" } +REQUIRED_WAIVER_FIELDS = { + "gate", "reason", "issue", "opened", "expires", "blocked_claim" +} SOFTWARE_ONLY_GATES = ( "model-download-runtime", "external-evaluation", @@ -24,9 +27,99 @@ ISSUE_URL = re.compile( r"^https://github\.com/elder-plinius/OBLITERATUS/issues/[1-9][0-9]*$", ) MAX_STALE_EXCEPTION_DAYS = 30 +MAX_ENVIRONMENT_WAIVER_DAYS = 30 -def validate(policy_path: Path, quality_path: Path, workflow_path: Path) -> list[str]: +def validate_environment_waivers( + policy: dict, + *, + today: date | None = None, +) -> tuple[dict[str, dict], list[str]]: + """Return active gate waivers and structural/lifetime errors.""" + + errors: list[str] = [] + active: dict[str, dict] = {} + today = today or date.today() + gates = policy.get("gates", []) + gate_ids = { + gate.get("id") + for gate in gates + if isinstance(gate, dict) and isinstance(gate.get("id"), str) + } + waivers = policy.get("environment_waivers", []) + if not isinstance(waivers, list): + return {}, ["conditional policy environment_waivers must be a list"] + + seen: set[str] = set() + for index, waiver in enumerate(waivers): + label = f"environment waiver {index}" + if not isinstance(waiver, dict): + errors.append(f"{label} must be an object") + continue + missing = REQUIRED_WAIVER_FIELDS - set(waiver) + if missing: + errors.append(f"{label} is missing fields: {sorted(missing)}") + continue + + gate_id = waiver["gate"] + if not isinstance(gate_id, str) or not gate_id: + errors.append(f"{label} gate must be a non-empty string") + continue + if gate_id in seen: + errors.append(f"duplicate environment waiver gate: {gate_id}") + continue + seen.add(gate_id) + valid = True + if gate_id not in gate_ids: + errors.append(f"environment waiver references unknown gate: {gate_id}") + valid = False + if gate_id in SOFTWARE_ONLY_GATES: + errors.append(f"software-only gate may not use an environment waiver: {gate_id}") + valid = False + + for field in ("reason", "blocked_claim"): + value = waiver[field] + if not isinstance(value, str) or not value.strip(): + errors.append(f"environment waiver {gate_id} {field} must be non-empty") + valid = False + issue = waiver["issue"] + if not isinstance(issue, str) or ISSUE_URL.fullmatch(issue) is None: + errors.append(f"environment waiver {gate_id} issue must be a canonical issue URL") + valid = False + + try: + opened = date.fromisoformat(waiver["opened"]) + expires = date.fromisoformat(waiver["expires"]) + except (TypeError, ValueError): + errors.append(f"environment waiver {gate_id} opened/expires must be ISO dates") + continue + if opened > today: + errors.append(f"environment waiver {gate_id} opens in the future: {opened}") + valid = False + if expires < opened: + errors.append(f"environment waiver {gate_id} expires before it opens") + valid = False + elif expires > opened + timedelta(days=MAX_ENVIRONMENT_WAIVER_DAYS): + errors.append( + f"environment waiver {gate_id} exceeds {MAX_ENVIRONMENT_WAIVER_DAYS} days", + ) + valid = False + if expires < today: + errors.append(f"environment waiver {gate_id} expired on {expires}") + valid = False + + if valid: + active[gate_id] = waiver + return active, errors + + +def validate( + policy_path: Path, + quality_path: Path, + workflow_path: Path, + *, + today: date | None = None, +) -> list[str]: errors: list[str] = [] policy = json.loads(policy_path.read_text()) quality = json.loads(quality_path.read_text()) @@ -44,6 +137,9 @@ def validate(policy_path: Path, quality_path: Path, workflow_path: Path) -> list by_id: dict[str, dict] = {} for index, gate in enumerate(gates): + if not isinstance(gate, dict): + errors.append(f"gate {index} must be an object") + continue missing = REQUIRED_GATE_FIELDS - set(gate) if missing: errors.append(f"gate {index} is missing fields: {sorted(missing)}") @@ -77,6 +173,8 @@ def validate(policy_path: Path, quality_path: Path, workflow_path: Path) -> list for token in required_workflow_tokens: if token not in workflow: errors.append(f"conditional workflow is missing {token!r}") + _, waiver_errors = validate_environment_waivers(policy, today=today) + errors.extend(waiver_errors) return errors diff --git a/scripts/conditional_gate_summary.py b/scripts/conditional_gate_summary.py index 6ac1913..9ef50f8 100644 --- a/scripts/conditional_gate_summary.py +++ b/scripts/conditional_gate_summary.py @@ -6,24 +6,49 @@ from __future__ import annotations import argparse import json import os -from datetime import datetime, timezone +from datetime import date, datetime, timezone from pathlib import Path +try: + from scripts.check_conditional_policy import validate_environment_waivers +except ModuleNotFoundError: # Direct execution adds scripts/, not the repository root. + from check_conditional_policy import validate_environment_waivers -def summarize(policy: dict, results: dict, selected: dict) -> tuple[list[dict], list[str]]: + +def summarize( + policy: dict, + results: dict, + selected: dict, + *, + today: date | None = None, +) -> tuple[list[dict], list[str]]: """Return per-gate rows and selected-job failures.""" - failures: list[str] = [] + waivers, waiver_errors = validate_environment_waivers(policy, today=today) + failures = [f"waiver policy: {error}" for error in waiver_errors] rows: list[dict] = [] failed_jobs: set[str] = set() for gate in policy["gates"]: job = gate["job"] is_selected = bool(selected.get(job, False)) result = results.get(job, "unknown") - status = result if is_selected else "not_selected_no_fresh_evidence" + waiver = waivers.get(gate["id"]) + if is_selected: + status = result + elif waiver is not None: + status = "waived_no_support_claim" + else: + status = "not_selected_no_fresh_evidence" if is_selected and result != "success" and job not in failed_jobs: failures.append(f"{job}: selected but result was {result}") failed_jobs.add(job) - rows.append({"gate": gate["id"], "job": job, "selected": is_selected, "status": status}) + row = {"gate": gate["id"], "job": job, "selected": is_selected, "status": status} + if not is_selected and waiver is not None: + row["waiver"] = { + "issue": waiver["issue"], + "expires": waiver["expires"], + "blocked_claim": waiver["blocked_claim"], + } + rows.append(row) return rows, failures @@ -50,8 +75,23 @@ def main() -> int: args.output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") summary_path = os.environ.get("GITHUB_STEP_SUMMARY") if summary_path: - lines = ["## Conditional test evidence", "", "| Gate | Job | Status |", "|---|---|---|"] - lines.extend(f"| {row['gate']} | {row['job']} | {row['status']} |" for row in rows) + lines = [ + "## Conditional test evidence", + "", + "| Gate | Job | Status | Constraint |", + "|---|---|---|---|", + ] + for row in rows: + waiver = row.get("waiver") + constraint = "" + if waiver is not None: + constraint = ( + f"No support claim; expires {waiver['expires']}; " + f"[tracker]({waiver['issue']})" + ) + lines.append( + f"| {row['gate']} | {row['job']} | {row['status']} | {constraint} |", + ) lines.extend(["", f"Evidence becomes stale after {policy['maximum_evidence_age_days']} days."]) with Path(summary_path).open("a") as handle: handle.write("\n".join(lines) + "\n") diff --git a/tests/test_conditional_gate_scripts.py b/tests/test_conditional_gate_scripts.py index 92d1ad0..54a1c8d 100644 --- a/tests/test_conditional_gate_scripts.py +++ b/tests/test_conditional_gate_scripts.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +from datetime import date from pathlib import Path from scripts import check_conditional_policy @@ -11,6 +12,20 @@ from scripts import run_conditional_gate ROOT = Path(__file__).parents[1] +TODAY = date(2026, 8, 16) + + +def _waiver(gate: str, **overrides) -> dict: + value = { + "gate": gate, + "reason": "No runner is currently attached.", + "issue": "https://github.com/elder-plinius/OBLITERATUS/issues/110", + "opened": "2026-08-16", + "expires": "2026-09-15", + "blocked_claim": f"{gate} support is not claimed.", + } + value.update(overrides) + return value def test_committed_conditional_policy_is_complete(): @@ -18,6 +33,7 @@ def test_committed_conditional_policy_is_complete(): ROOT / "ci" / "conditional-test-policy.json", ROOT / "ci" / "test-quality-policy.json", ROOT / ".github" / "workflows" / "conditional-tests.yml", + today=TODAY, ) == [] @@ -92,3 +108,139 @@ def test_summary_rejects_selected_failure_and_marks_unselected_stale(): assert rows[0]["status"] == "failure" assert rows[1]["status"] == "failure" assert rows[2]["status"] == "not_selected_no_fresh_evidence" + + +def test_summary_records_active_waiver_and_selected_success_overrides_it(): + policy = { + "gates": [{"id": "cuda-runtime", "job": "cuda"}], + "environment_waivers": [_waiver("cuda-runtime")], + } + + rows, failures = conditional_gate_summary.summarize( + policy, + {"cuda": "skipped"}, + {"cuda": False}, + today=TODAY, + ) + assert failures == [] + assert rows == [{ + "gate": "cuda-runtime", + "job": "cuda", + "selected": False, + "status": "waived_no_support_claim", + "waiver": { + "issue": "https://github.com/elder-plinius/OBLITERATUS/issues/110", + "expires": "2026-09-15", + "blocked_claim": "cuda-runtime support is not claimed.", + }, + }] + + rows, failures = conditional_gate_summary.summarize( + policy, + {"cuda": "success"}, + {"cuda": True}, + today=TODAY, + ) + assert failures == [] + assert rows[0]["status"] == "success" + assert "waiver" not in rows[0] + + +def test_environment_waiver_validation_rejects_duplicate_expired_and_overlong_records(): + policy = { + "gates": [ + {"id": "cuda-runtime"}, + {"id": "mps-runtime"}, + {"id": "mlx-runtime"}, + ], + "environment_waivers": [ + _waiver("cuda-runtime"), + _waiver("cuda-runtime"), + _waiver("mps-runtime", opened="2026-07-01", expires="2026-07-31"), + _waiver("mlx-runtime", expires="2026-09-16"), + ], + } + + active, errors = check_conditional_policy.validate_environment_waivers( + policy, + today=TODAY, + ) + + assert active == {"cuda-runtime": policy["environment_waivers"][0]} + assert "duplicate environment waiver gate: cuda-runtime" in errors + assert "environment waiver mps-runtime expired on 2026-07-31" in errors + assert "environment waiver mlx-runtime exceeds 30 days" in errors + + +def test_environment_waiver_validation_rejects_malformed_or_software_records(): + policy = { + "gates": [ + {"id": "network-services"}, + {"id": "remote-execution"}, + ], + "environment_waivers": [ + _waiver("network-services"), + _waiver("remote-execution", issue="not-an-issue", blocked_claim=""), + "not-an-object", + ], + } + + active, errors = check_conditional_policy.validate_environment_waivers( + policy, + today=TODAY, + ) + + assert active == {} + assert "software-only gate may not use an environment waiver: network-services" in errors + assert "environment waiver remote-execution issue must be a canonical issue URL" in errors + assert "environment waiver remote-execution blocked_claim must be non-empty" in errors + assert "environment waiver 2 must be an object" in errors + + +def test_environment_waiver_validation_rejects_missing_future_and_reversed_dates(): + missing = _waiver("remote-execution") + missing.pop("reason") + policy = { + "gates": [ + {"id": "remote-execution"}, + {"id": "mps-runtime"}, + {"id": "mlx-runtime"}, + ], + "environment_waivers": [ + missing, + _waiver("mps-runtime", opened="2026-08-17", expires="2026-09-15"), + _waiver("mlx-runtime", opened="2026-08-16", expires="2026-08-15"), + ], + } + + active, errors = check_conditional_policy.validate_environment_waivers( + policy, + today=TODAY, + ) + + assert active == {} + assert "environment waiver 0 is missing fields: ['reason']" in errors + assert "environment waiver mps-runtime opens in the future: 2026-08-17" in errors + assert "environment waiver mlx-runtime expires before it opens" in errors + assert "environment waiver mlx-runtime expired on 2026-08-15" in errors + + +def test_summary_fails_an_expired_waiver(): + policy = { + "gates": [{"id": "cuda-runtime", "job": "cuda"}], + "environment_waivers": [ + _waiver("cuda-runtime", opened="2026-07-01", expires="2026-07-31"), + ], + } + + rows, failures = conditional_gate_summary.summarize( + policy, + {"cuda": "skipped"}, + {"cuda": False}, + today=TODAY, + ) + + assert rows[0]["status"] == "not_selected_no_fresh_evidence" + assert failures == [ + "waiver policy: environment waiver cuda-runtime expired on 2026-07-31", + ]