diff --git a/Makefile b/Makefile index b453c6238..f3ee165ce 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ export TMPDIR := /var/tmp PYTEST_BASETEMP_INIT := BASETEMP=$$(mktemp -d /var/tmp/solstone-pytest-XXXXXX); trap 'rm -rf "$$BASETEMP"' EXIT INT TERM; PYTEST_BASETEMP_FLAG := --basetemp "$$BASETEMP" -.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny audit openapi check-openapi contract check-contract journal-resolution-vectors check-journal-resolution-vectors dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-cogitate-prompts smoke-cogitate release release-test FORCE +.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-cogitate-prompts smoke-cogitate release release-test FORCE # Default target - install package in editable mode all: install @@ -684,6 +684,10 @@ openapi: check-openapi: .installed $(VENV_BIN)/python scripts/check_openapi_contract.py $(VENV_BIN)/python scripts/build_openapi_contract.py --check + $(MAKE) check-openapi-observer-client-contract + +check-openapi-observer-client-contract: .installed + $(VENV_BIN)/python scripts/check_observer_client_contract_bundle.py journal-resolution-vectors: $(VENV_BIN)/python scripts/build_journal_resolution_vectors.py diff --git a/scripts/check_observer_client_contract_bundle.py b/scripts/check_observer_client_contract_bundle.py new file mode 100644 index 000000000..70a85197c --- /dev/null +++ b/scripts/check_observer_client_contract_bundle.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Check the generated observer-client OpenAPI contract bundle.""" + +from __future__ import annotations + +import sys +from collections.abc import Callable +from pathlib import Path + +from solstone.convey.contract.observer_bundle import ( + BUNDLE_REL_DIR, + stale_bundle_paths, +) +from solstone.convey.contract.observer_bundle_compatibility import ( + check_bundle_compatibility, +) +from solstone.convey.contract.observer_bundle_verification import ( + check_consumer_audit_coverage, + verify_committed_bundle, +) + +ROOT = Path(__file__).resolve().parent.parent + + +def _staleness_failures() -> list[str]: + stale = stale_bundle_paths(ROOT) + if not stale: + return [] + paths = ", ".join(str(path) for path in stale) + return [ + "observer client contract bundle staleness failed: " + f"{paths}. Recovery: run make openapi." + ] + + +def _manifest_failures() -> list[str]: + verify_committed_bundle(ROOT) + return [] + + +def _compatibility_failures() -> list[str]: + return check_bundle_compatibility(ROOT) + + +def _coverage_failures() -> list[str]: + return check_consumer_audit_coverage(ROOT) + + +def _run_gate(name: str, fn: Callable[[], list[str]]) -> bool: + try: + failures = fn() + except Exception as exc: + failures = [ + f"observer client contract {name} failed: {exc}. " + "Recovery: run make openapi, repair the bundle, or apply the " + "required SemVer bump." + ] + if not failures: + print(f"observer-client-contract {name}: pass") + return False + for failure in failures: + print(failure, file=sys.stderr) + return True + + +def main() -> int: + failed = False + gates: tuple[tuple[str, Callable[[], list[str]]], ...] = ( + ("staleness", _staleness_failures), + ("manifest", _manifest_failures), + ("compatibility", _compatibility_failures), + ("windows-linux-coverage", _coverage_failures), + ) + for name, fn in gates: + failed = _run_gate(name, fn) or failed + if failed: + print( + f"observer-client-contract: failed for {BUNDLE_REL_DIR}.", + file=sys.stderr, + ) + return 1 + print(f"observer-client-contract: pass for {BUNDLE_REL_DIR}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/export_observer_client_contract_bundle.py b/scripts/export_observer_client_contract_bundle.py new file mode 100644 index 000000000..faf832bb5 --- /dev/null +++ b/scripts/export_observer_client_contract_bundle.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Export the committed observer-client OpenAPI contract bundle.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from solstone.convey.contract.observer_bundle_export import ( + BundleExportRefused, + export_bundle, +) + +ROOT = Path(__file__).resolve().parent.parent + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("destination", type=Path, help="New destination directory.") + args = parser.parse_args() + + try: + destination = export_bundle(args.destination, ROOT) + except BundleExportRefused as exc: + print(str(exc), file=sys.stderr) + return 1 + print(f"exported observer client contract bundle to {destination}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())