From 6c25155f883af970bf3440e85c28b61d17bfead7 Mon Sep 17 00:00:00 2001 From: Wolfie Date: Sun, 30 Aug 2026 18:33:33 -0700 Subject: [PATCH 1/4] refactor: consume WolfXL Guard contract --- .github/workflows/ci.yml | 33 +++++++++++++ src/spreadsheet_guard/guard.py | 75 ++++++++++++++++++----------- tests/test_guard_cli.py | 87 ++++++++++++++++++++++++++++------ 3 files changed, 152 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d60221f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + package: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.13"] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d + with: + version: "0.12.3" + - name: Install package and test dependencies + run: uv pip install --system -e ".[dev]" + - name: Check package + run: | + ruff check src tests + mypy src + pytest -q + uv build diff --git a/src/spreadsheet_guard/guard.py b/src/spreadsheet_guard/guard.py index 7abb538..860641e 100644 --- a/src/spreadsheet_guard/guard.py +++ b/src/spreadsheet_guard/guard.py @@ -9,16 +9,19 @@ from typing import Any, cast +EXPECTED_GUARD_CONTRACT_VERSION = "wolfxl-guard-contract-v1" + + class GuardError(RuntimeError): """Base error for the public Guard surface.""" class GuardUnavailableError(GuardError): - """Raised when the WolfXL Guard runtime is not installed.""" + """Raised when a compatible WolfXL Guard runtime is not installed.""" class GuardExecutionError(GuardError): - """Raised when WolfXL does not produce a valid Guard report.""" + """Raised when WolfXL does not produce a valid Guard result.""" @dataclass(frozen=True) @@ -30,22 +33,31 @@ class GuardOutcome: report: Mapping[str, Any] -GuardFunction = Callable[..., dict[str, Any]] +GuardFunction = Callable[..., Mapping[str, object]] def _load_wolfxl_guard() -> GuardFunction: + """Resolve only WolfXL's versioned public contract, never an internal path.""" + try: - module = import_module("wolfxl.operations") + module = import_module("wolfxl.guard_contract") except ImportError as exc: raise GuardUnavailableError( - "WolfXL Guard requires the WolfXL Commercial runtime. " - "WolfXL Community does not include the operations SDK. " - "Request evaluation access at https://wolfxl.com" + "Spreadsheet Guard requires a WolfXL runtime exposing " + f"{EXPECTED_GUARD_CONTRACT_VERSION}" ) from exc - run_guard = getattr(module, "run_guard", None) - if not callable(run_guard): - raise GuardUnavailableError("the installed WolfXL runtime does not expose Guard") - return cast(GuardFunction, run_guard) + actual_version = getattr(module, "GUARD_CONTRACT_VERSION", None) + if actual_version != EXPECTED_GUARD_CONTRACT_VERSION: + raise GuardUnavailableError( + "installed WolfXL Guard contract is incompatible; " + f"expected {EXPECTED_GUARD_CONTRACT_VERSION}, got {actual_version!r}" + ) + runner = getattr(module, "run_guard_contract", None) + if not callable(runner): + raise GuardUnavailableError( + "installed WolfXL runtime does not expose run_guard_contract" + ) + return cast(GuardFunction, runner) def guard_workbooks( @@ -55,23 +67,15 @@ def guard_workbooks( *, policy: Mapping[str, Any] | None = None, ) -> GuardOutcome: - """Compare two workbooks without modifying either input. + """Compare two workbooks through WolfXL's pinned Guard contract. - WolfXL owns workbook inspection and policy semantics. This package owns only - the stable public invocation and fail-closed result contract. + This package owns the stable command/package identity. WolfXL owns policy, + inspection, report validation, and report-artifact binding. """ runner = _load_wolfxl_guard() - arguments: dict[str, Any] = { - "before": before, - "after": after, - "output": output, - } - if policy is not None: - arguments["policy"] = policy - try: - report = runner(**arguments) + result = runner(before=before, after=after, output=output, policy=policy) except Exception as exc: if isinstance(exc, GuardError): raise @@ -79,12 +83,27 @@ def guard_workbooks( f"WolfXL Guard failed ({type(exc).__name__}): {exc}" ) from exc - if not isinstance(report, Mapping): - raise GuardExecutionError("WolfXL Guard returned a non-object report") - status = report.get("status") + if not isinstance(result, Mapping): + raise GuardExecutionError("WolfXL Guard contract returned a non-object result") + if result.get("contract_version") != EXPECTED_GUARD_CONTRACT_VERSION: + raise GuardExecutionError("WolfXL Guard result contract version drifted") + status = result.get("status") if status not in {"passed", "failed", "unassessed"}: raise GuardExecutionError(f"WolfXL Guard returned invalid status {status!r}") - if not output.is_file(): + report = result.get("report") + if not isinstance(report, Mapping) or report.get("status") != status: + raise GuardExecutionError("WolfXL Guard returned an inconsistent report") + output_path_value = result.get("output_path") + if not isinstance(output_path_value, str): + raise GuardExecutionError("WolfXL Guard did not return its report path") + output_path = Path(output_path_value).resolve(strict=False) + if output_path != output.expanduser().resolve(strict=False): + raise GuardExecutionError("WolfXL Guard returned an unbound report path") + if not output_path.is_file(): raise GuardExecutionError("WolfXL Guard did not write the requested report") - return GuardOutcome(status=status, output_path=output, report=dict(report)) + return GuardOutcome( + status=str(status), + output_path=output_path, + report=dict(report), + ) diff --git a/tests/test_guard_cli.py b/tests/test_guard_cli.py index 7ca4a76..135a288 100644 --- a/tests/test_guard_cli.py +++ b/tests/test_guard_cli.py @@ -2,16 +2,25 @@ import json from pathlib import Path +from types import ModuleType from typing import Any import pytest import spreadsheet_guard.cli as cli_module import spreadsheet_guard.guard as guard_module -from spreadsheet_guard.guard import GuardExecutionError, GuardOutcome, guard_workbooks +from spreadsheet_guard.guard import ( + GuardExecutionError, + GuardOutcome, + GuardUnavailableError, + guard_workbooks, +) -def test_guard_workbooks_delegates_read_only_comparison(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_guard_workbooks_delegates_to_versioned_contract( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: before = tmp_path / "before.xlsx" after = tmp_path / "after.xlsx" output = tmp_path / "guard.json" @@ -19,40 +28,83 @@ def test_guard_workbooks_delegates_read_only_comparison(tmp_path: Path, monkeypa after.write_bytes(b"after") received: dict[str, Any] = {} - def fake_guard(**kwargs: Any) -> dict[str, Any]: + def fake_guard(**kwargs: Any) -> dict[str, object]: received.update(kwargs) report = {"schema_version": 1, "status": "passed"} kwargs["output"].write_text(json.dumps(report)) - return report + return { + "contract_version": "wolfxl-guard-contract-v1", + "status": "passed", + "output_path": str(kwargs["output"].resolve()), + "report": report, + } monkeypatch.setattr(guard_module, "_load_wolfxl_guard", lambda: fake_guard) outcome = guard_workbooks(before, after, output) assert outcome.status == "passed" - assert outcome.output_path == output - assert received == {"before": before, "after": after, "output": output} + assert outcome.output_path == output.resolve() + assert received == { + "before": before, + "after": after, + "output": output, + "policy": None, + } assert before.read_bytes() == b"before" assert after.read_bytes() == b"after" -def test_guard_workbooks_fails_closed_on_invalid_report( - tmp_path: Path, +@pytest.mark.parametrize( + ("version", "has_runner"), + [ + (None, True), + ("wolfxl-guard-contract-v0", True), + ("wolfxl-guard-contract-v1", False), + ], +) +def test_loader_fails_closed_on_contract_drift( + version: object, + has_runner: bool, monkeypatch: pytest.MonkeyPatch, ) -> None: - output = tmp_path / "guard.json" + module = ModuleType("wolfxl.guard_contract") + if version is not None: + module.GUARD_CONTRACT_VERSION = version + if has_runner: + module.run_guard_contract = lambda **kwargs: kwargs + monkeypatch.setattr(guard_module, "import_module", lambda name: module) - def fake_guard(**kwargs: Any) -> dict[str, Any]: - kwargs["output"].write_text("{}") - return {"status": "unknown"} + with pytest.raises(GuardUnavailableError): + guard_module._load_wolfxl_guard() - monkeypatch.setattr(guard_module, "_load_wolfxl_guard", lambda: fake_guard) + +def test_guard_workbooks_fails_closed_on_invalid_contract_result( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + output = tmp_path / "guard.json" + output.write_text("{}") + + monkeypatch.setattr( + guard_module, + "_load_wolfxl_guard", + lambda: lambda **kwargs: { + "contract_version": "wolfxl-guard-contract-v1", + "status": "unknown", + "output_path": str(output), + "report": {}, + }, + ) with pytest.raises(GuardExecutionError, match="invalid status"): guard_workbooks(tmp_path / "before.xlsx", tmp_path / "after.xlsx", output) -@pytest.mark.parametrize(("status", "expected_code"), [("passed", 0), ("failed", 1), ("unassessed", 1)]) +@pytest.mark.parametrize( + ("status", "expected_code"), + [("passed", 0), ("failed", 1), ("unassessed", 1)], +) def test_guard_cli_exit_code_tracks_guard_status( status: str, expected_code: int, @@ -78,7 +130,12 @@ def fake_guard_workbooks( monkeypatch.setattr(cli_module, "guard_workbooks", fake_guard_workbooks) code = cli_module.main( - [str(tmp_path / "before.xlsx"), str(tmp_path / "after.xlsx"), "--output", str(output)] + [ + str(tmp_path / "before.xlsx"), + str(tmp_path / "after.xlsx"), + "--output", + str(output), + ] ) assert code == expected_code From 8100b6ab74f3291d93edff3f3d807c564627face Mon Sep 17 00:00:00 2001 From: Wolfie Date: Sun, 30 Aug 2026 18:44:01 -0700 Subject: [PATCH 2/4] style: align focused CI lint contract --- src/spreadsheet_guard/guard.py | 1 - tests/test_guard_cli.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spreadsheet_guard/guard.py b/src/spreadsheet_guard/guard.py index 860641e..f5b80a9 100644 --- a/src/spreadsheet_guard/guard.py +++ b/src/spreadsheet_guard/guard.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Any, cast - EXPECTED_GUARD_CONTRACT_VERSION = "wolfxl-guard-contract-v1" diff --git a/tests/test_guard_cli.py b/tests/test_guard_cli.py index 135a288..f10d2a1 100644 --- a/tests/test_guard_cli.py +++ b/tests/test_guard_cli.py @@ -6,7 +6,6 @@ from typing import Any import pytest - import spreadsheet_guard.cli as cli_module import spreadsheet_guard.guard as guard_module from spreadsheet_guard.guard import ( @@ -125,7 +124,11 @@ def fake_guard_workbooks( assert after == tmp_path / "after.xlsx" assert requested_output == output assert policy is None - return GuardOutcome(status=status, output_path=output, report={"status": status}) + return GuardOutcome( + status=status, + output_path=output, + report={"status": status}, + ) monkeypatch.setattr(cli_module, "guard_workbooks", fake_guard_workbooks) From fd6a84728f32e8435d405f173b23f6328b107643 Mon Sep 17 00:00:00 2001 From: Wolfie Date: Sun, 30 Aug 2026 18:47:20 -0700 Subject: [PATCH 3/4] style: preserve first-party import group --- tests/test_guard_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_guard_cli.py b/tests/test_guard_cli.py index f10d2a1..3726f13 100644 --- a/tests/test_guard_cli.py +++ b/tests/test_guard_cli.py @@ -6,6 +6,7 @@ from typing import Any import pytest + import spreadsheet_guard.cli as cli_module import spreadsheet_guard.guard as guard_module from spreadsheet_guard.guard import ( From ccc9b32ab1541c5a7e464561c780833df21daa01 Mon Sep 17 00:00:00 2001 From: Wolfie Date: Sun, 30 Aug 2026 18:59:11 -0700 Subject: [PATCH 4/4] fix: reject non-text Guard statuses fail-closed --- src/spreadsheet_guard/guard.py | 6 +++++- tests/test_guard_cli.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/spreadsheet_guard/guard.py b/src/spreadsheet_guard/guard.py index f5b80a9..1eb91f7 100644 --- a/src/spreadsheet_guard/guard.py +++ b/src/spreadsheet_guard/guard.py @@ -87,7 +87,11 @@ def guard_workbooks( if result.get("contract_version") != EXPECTED_GUARD_CONTRACT_VERSION: raise GuardExecutionError("WolfXL Guard result contract version drifted") status = result.get("status") - if status not in {"passed", "failed", "unassessed"}: + if not isinstance(status, str) or status not in { + "passed", + "failed", + "unassessed", + }: raise GuardExecutionError(f"WolfXL Guard returned invalid status {status!r}") report = result.get("report") if not isinstance(report, Mapping) or report.get("status") != status: diff --git a/tests/test_guard_cli.py b/tests/test_guard_cli.py index 3726f13..aa8c32d 100644 --- a/tests/test_guard_cli.py +++ b/tests/test_guard_cli.py @@ -79,7 +79,9 @@ def test_loader_fails_closed_on_contract_drift( guard_module._load_wolfxl_guard() +@pytest.mark.parametrize("invalid_status", ["unknown", [], {}]) def test_guard_workbooks_fails_closed_on_invalid_contract_result( + invalid_status: object, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -91,7 +93,7 @@ def test_guard_workbooks_fails_closed_on_invalid_contract_result( "_load_wolfxl_guard", lambda: lambda **kwargs: { "contract_version": "wolfxl-guard-contract-v1", - "status": "unknown", + "status": invalid_status, "output_path": str(output), "report": {}, },