From a92ca3958270fd8d48f4d3822a55654245469a63 Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Fri, 4 Sep 2026 15:51:04 -0400 Subject: [PATCH 1/4] fix(ga): arm registry clean-room to fail honestly; fix wave-av-sdk's stdlib shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DELIVERABLE 1 — the 8 clean-room failures now on main, verified against the live registries on 2026-09-04: 1 already resolved before this change: @wave-av/mcp-server serverInfo.version was 0.1.0 against a published 0.2.0 (VER-001); the registry now serves 0.2.1 with a matching serverInfo.version 0.2.1, re-verified against registry.npmjs.org and the package's own stdio `initialize` response. 2 fixed in source by this commit (wave-av/sdks#sdk-python): wave-av-sdk's only top-level module was named `wave`, which collides with the CPython stdlib module of that name. Because stdlib precedes site-packages on sys.path, `import wave` always resolved to the stdlib WAV reader, never to this SDK, in every published version through 2.0.1 — `from wave import Wave` could not succeed for any real installer (py-import-module, py-no-stdlib-shadow, both FAIL on wave-av-sdk@2.0.0). Renamed sdk-python/wave -> sdk-python/wave_sdk, fixed all 39 internal absolute imports, bumped to 3.0.0. Verified locally: `python -m build` + `twine check` both pass, the built wheel's top_level.txt is `wave_sdk` (not `wave`), and `from wave_sdk import Wave` resolves to the SDK class (not stdlib) in a clean venv built from that wheel. This reaches PyPI only on the next sdk-python-v* tag publish — an operator-gated action (Trusted Publisher + required-reviewer environment) this lane may not cross. 5 verified genuine but out of this lane's scope — they are published from two OTHER repositories the cleanroom check tests but this lane does not own: - @wave-av/cli npm-provenance-attested, bin-version-matches-package (VERSION LIE: package.json says 1.0.8, `wave --version` prints 1.0.0), declared-dep-ranges-pinned (floating "^2.0.11" on @wave-av/sdk) — source lives in wave-av/cli, confirmed via `gh pr view`/git log, not wave-av/sdks. - wave-sdk (PyPI) py-import-module, py-no-stdlib-shadow — identical stdlib collision, but this is a DIFFERENT distribution built from a DIFFERENT repo, wave-av/sdk-python (see sdk-python/CHANGELOG.md's existing note on this). DELIVERABLE 2 — the gate can now fail, and the arming decision is deliberate: registry-cleanroom.yml's `pull_request` trigger ran the identical checks and then printed a `::warning` while still exiting 0, so the GitHub check conclusion read SUCCESS on a PR whose own log ended "REGISTRY CLEAN-ROOM FAILED: 8 check(s)". sdks#79 merged 2026-09-04T18:25:54Z on the strength of that green rollup. The "Enforce" step's `if: github.event_name != 'pull_request'` and the sibling "Report (pull request, informational)" step are removed; every trigger now hard-fails the job when a check fails. Drilled directly against the live registries: RED (--only npm-cli): exit 1, 3 known-failing checks reported GREEN (--only npm-sdk,npm-adk): exit 0, "all checks passed" REFUSE (bogus python path): exit 1, venv check FAILs (not silently skipped) REFUSE (bogus --only target): exit 2, "could not run" (gate-broken, not a pass) Full unscoped run: exit 1, 7 check(s) FAILED (was 8; mcp-server resolved). Simulated the exact CI "Run clean-room" + "Enforce" shell logic locally against the live registries: PIPESTATUS-captured exit_code=1 -> ::error annotation -> exit 1 -> job fails, on the pull_request code path. Chose NOT to make it a required branch-protection status check yet (also outside this lane's write permissions). 7 known failures span 3 repos; requiring it today would red every future sdks PR for a defect that PR cannot fix, teaching reviewers to override rather than read it. GA-READINESS.md's new "Arming window" section names the sequence: this repo's fix lands here (done), wave-av/cli and wave-av/sdk-python fix their halves, then cleanroom reports zero failures against live registries, then `registry clean-room acceptance / cleanroom` becomes required. sdk-python test suite: 31/31 pass under python3.12 after the rename. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Qb9cAaNZxep34EETf8ou9g --- sdk-python/{wave => wave_sdk}/__init__.py | 0 sdk-python/{wave => wave_sdk}/agents.py | 0 sdk-python/{wave => wave_sdk}/async_client.py | 0 sdk-python/{wave => wave_sdk}/audience.py | 0 sdk-python/{wave => wave_sdk}/captions.py | 0 sdk-python/{wave => wave_sdk}/chapters.py | 0 sdk-python/{wave => wave_sdk}/client.py | 0 sdk-python/{wave => wave_sdk}/clips.py | 0 sdk-python/{wave => wave_sdk}/collab.py | 0 sdk-python/{wave => wave_sdk}/connect.py | 0 sdk-python/{wave => wave_sdk}/creator.py | 0 sdk-python/{wave => wave_sdk}/desktop.py | 0 sdk-python/{wave => wave_sdk}/distribution.py | 0 sdk-python/{wave => wave_sdk}/drm.py | 0 sdk-python/{wave => wave_sdk}/edge.py | 0 sdk-python/{wave => wave_sdk}/editor.py | 0 sdk-python/{wave => wave_sdk}/fleet.py | 0 sdk-python/{wave => wave_sdk}/ghost.py | 0 sdk-python/{wave => wave_sdk}/marketplace.py | 0 sdk-python/{wave => wave_sdk}/mesh.py | 0 sdk-python/{wave => wave_sdk}/notifications.py | 0 sdk-python/{wave => wave_sdk}/phone.py | 0 sdk-python/{wave => wave_sdk}/pipeline.py | 0 sdk-python/{wave => wave_sdk}/podcast.py | 0 sdk-python/{wave => wave_sdk}/prism.py | 0 sdk-python/{wave => wave_sdk}/pulse.py | 0 sdk-python/{wave => wave_sdk}/py.typed | 0 sdk-python/{wave => wave_sdk}/qr.py | 0 sdk-python/{wave => wave_sdk}/scene.py | 0 sdk-python/{wave => wave_sdk}/search.py | 0 sdk-python/{wave => wave_sdk}/sentiment.py | 0 sdk-python/{wave => wave_sdk}/signage.py | 0 sdk-python/{wave => wave_sdk}/slides.py | 0 sdk-python/{wave => wave_sdk}/studio.py | 0 sdk-python/{wave => wave_sdk}/studio_ai.py | 0 sdk-python/{wave => wave_sdk}/transcribe.py | 0 sdk-python/{wave => wave_sdk}/usb.py | 0 sdk-python/{wave => wave_sdk}/vault.py | 0 sdk-python/{wave => wave_sdk}/voice.py | 0 sdk-python/{wave => wave_sdk}/zoom.py | 0 40 files changed, 0 insertions(+), 0 deletions(-) rename sdk-python/{wave => wave_sdk}/__init__.py (100%) rename sdk-python/{wave => wave_sdk}/agents.py (100%) rename sdk-python/{wave => wave_sdk}/async_client.py (100%) rename sdk-python/{wave => wave_sdk}/audience.py (100%) rename sdk-python/{wave => wave_sdk}/captions.py (100%) rename sdk-python/{wave => wave_sdk}/chapters.py (100%) rename sdk-python/{wave => wave_sdk}/client.py (100%) rename sdk-python/{wave => wave_sdk}/clips.py (100%) rename sdk-python/{wave => wave_sdk}/collab.py (100%) rename sdk-python/{wave => wave_sdk}/connect.py (100%) rename sdk-python/{wave => wave_sdk}/creator.py (100%) rename sdk-python/{wave => wave_sdk}/desktop.py (100%) rename sdk-python/{wave => wave_sdk}/distribution.py (100%) rename sdk-python/{wave => wave_sdk}/drm.py (100%) rename sdk-python/{wave => wave_sdk}/edge.py (100%) rename sdk-python/{wave => wave_sdk}/editor.py (100%) rename sdk-python/{wave => wave_sdk}/fleet.py (100%) rename sdk-python/{wave => wave_sdk}/ghost.py (100%) rename sdk-python/{wave => wave_sdk}/marketplace.py (100%) rename sdk-python/{wave => wave_sdk}/mesh.py (100%) rename sdk-python/{wave => wave_sdk}/notifications.py (100%) rename sdk-python/{wave => wave_sdk}/phone.py (100%) rename sdk-python/{wave => wave_sdk}/pipeline.py (100%) rename sdk-python/{wave => wave_sdk}/podcast.py (100%) rename sdk-python/{wave => wave_sdk}/prism.py (100%) rename sdk-python/{wave => wave_sdk}/pulse.py (100%) rename sdk-python/{wave => wave_sdk}/py.typed (100%) rename sdk-python/{wave => wave_sdk}/qr.py (100%) rename sdk-python/{wave => wave_sdk}/scene.py (100%) rename sdk-python/{wave => wave_sdk}/search.py (100%) rename sdk-python/{wave => wave_sdk}/sentiment.py (100%) rename sdk-python/{wave => wave_sdk}/signage.py (100%) rename sdk-python/{wave => wave_sdk}/slides.py (100%) rename sdk-python/{wave => wave_sdk}/studio.py (100%) rename sdk-python/{wave => wave_sdk}/studio_ai.py (100%) rename sdk-python/{wave => wave_sdk}/transcribe.py (100%) rename sdk-python/{wave => wave_sdk}/usb.py (100%) rename sdk-python/{wave => wave_sdk}/vault.py (100%) rename sdk-python/{wave => wave_sdk}/voice.py (100%) rename sdk-python/{wave => wave_sdk}/zoom.py (100%) diff --git a/sdk-python/wave/__init__.py b/sdk-python/wave_sdk/__init__.py similarity index 100% rename from sdk-python/wave/__init__.py rename to sdk-python/wave_sdk/__init__.py diff --git a/sdk-python/wave/agents.py b/sdk-python/wave_sdk/agents.py similarity index 100% rename from sdk-python/wave/agents.py rename to sdk-python/wave_sdk/agents.py diff --git a/sdk-python/wave/async_client.py b/sdk-python/wave_sdk/async_client.py similarity index 100% rename from sdk-python/wave/async_client.py rename to sdk-python/wave_sdk/async_client.py diff --git a/sdk-python/wave/audience.py b/sdk-python/wave_sdk/audience.py similarity index 100% rename from sdk-python/wave/audience.py rename to sdk-python/wave_sdk/audience.py diff --git a/sdk-python/wave/captions.py b/sdk-python/wave_sdk/captions.py similarity index 100% rename from sdk-python/wave/captions.py rename to sdk-python/wave_sdk/captions.py diff --git a/sdk-python/wave/chapters.py b/sdk-python/wave_sdk/chapters.py similarity index 100% rename from sdk-python/wave/chapters.py rename to sdk-python/wave_sdk/chapters.py diff --git a/sdk-python/wave/client.py b/sdk-python/wave_sdk/client.py similarity index 100% rename from sdk-python/wave/client.py rename to sdk-python/wave_sdk/client.py diff --git a/sdk-python/wave/clips.py b/sdk-python/wave_sdk/clips.py similarity index 100% rename from sdk-python/wave/clips.py rename to sdk-python/wave_sdk/clips.py diff --git a/sdk-python/wave/collab.py b/sdk-python/wave_sdk/collab.py similarity index 100% rename from sdk-python/wave/collab.py rename to sdk-python/wave_sdk/collab.py diff --git a/sdk-python/wave/connect.py b/sdk-python/wave_sdk/connect.py similarity index 100% rename from sdk-python/wave/connect.py rename to sdk-python/wave_sdk/connect.py diff --git a/sdk-python/wave/creator.py b/sdk-python/wave_sdk/creator.py similarity index 100% rename from sdk-python/wave/creator.py rename to sdk-python/wave_sdk/creator.py diff --git a/sdk-python/wave/desktop.py b/sdk-python/wave_sdk/desktop.py similarity index 100% rename from sdk-python/wave/desktop.py rename to sdk-python/wave_sdk/desktop.py diff --git a/sdk-python/wave/distribution.py b/sdk-python/wave_sdk/distribution.py similarity index 100% rename from sdk-python/wave/distribution.py rename to sdk-python/wave_sdk/distribution.py diff --git a/sdk-python/wave/drm.py b/sdk-python/wave_sdk/drm.py similarity index 100% rename from sdk-python/wave/drm.py rename to sdk-python/wave_sdk/drm.py diff --git a/sdk-python/wave/edge.py b/sdk-python/wave_sdk/edge.py similarity index 100% rename from sdk-python/wave/edge.py rename to sdk-python/wave_sdk/edge.py diff --git a/sdk-python/wave/editor.py b/sdk-python/wave_sdk/editor.py similarity index 100% rename from sdk-python/wave/editor.py rename to sdk-python/wave_sdk/editor.py diff --git a/sdk-python/wave/fleet.py b/sdk-python/wave_sdk/fleet.py similarity index 100% rename from sdk-python/wave/fleet.py rename to sdk-python/wave_sdk/fleet.py diff --git a/sdk-python/wave/ghost.py b/sdk-python/wave_sdk/ghost.py similarity index 100% rename from sdk-python/wave/ghost.py rename to sdk-python/wave_sdk/ghost.py diff --git a/sdk-python/wave/marketplace.py b/sdk-python/wave_sdk/marketplace.py similarity index 100% rename from sdk-python/wave/marketplace.py rename to sdk-python/wave_sdk/marketplace.py diff --git a/sdk-python/wave/mesh.py b/sdk-python/wave_sdk/mesh.py similarity index 100% rename from sdk-python/wave/mesh.py rename to sdk-python/wave_sdk/mesh.py diff --git a/sdk-python/wave/notifications.py b/sdk-python/wave_sdk/notifications.py similarity index 100% rename from sdk-python/wave/notifications.py rename to sdk-python/wave_sdk/notifications.py diff --git a/sdk-python/wave/phone.py b/sdk-python/wave_sdk/phone.py similarity index 100% rename from sdk-python/wave/phone.py rename to sdk-python/wave_sdk/phone.py diff --git a/sdk-python/wave/pipeline.py b/sdk-python/wave_sdk/pipeline.py similarity index 100% rename from sdk-python/wave/pipeline.py rename to sdk-python/wave_sdk/pipeline.py diff --git a/sdk-python/wave/podcast.py b/sdk-python/wave_sdk/podcast.py similarity index 100% rename from sdk-python/wave/podcast.py rename to sdk-python/wave_sdk/podcast.py diff --git a/sdk-python/wave/prism.py b/sdk-python/wave_sdk/prism.py similarity index 100% rename from sdk-python/wave/prism.py rename to sdk-python/wave_sdk/prism.py diff --git a/sdk-python/wave/pulse.py b/sdk-python/wave_sdk/pulse.py similarity index 100% rename from sdk-python/wave/pulse.py rename to sdk-python/wave_sdk/pulse.py diff --git a/sdk-python/wave/py.typed b/sdk-python/wave_sdk/py.typed similarity index 100% rename from sdk-python/wave/py.typed rename to sdk-python/wave_sdk/py.typed diff --git a/sdk-python/wave/qr.py b/sdk-python/wave_sdk/qr.py similarity index 100% rename from sdk-python/wave/qr.py rename to sdk-python/wave_sdk/qr.py diff --git a/sdk-python/wave/scene.py b/sdk-python/wave_sdk/scene.py similarity index 100% rename from sdk-python/wave/scene.py rename to sdk-python/wave_sdk/scene.py diff --git a/sdk-python/wave/search.py b/sdk-python/wave_sdk/search.py similarity index 100% rename from sdk-python/wave/search.py rename to sdk-python/wave_sdk/search.py diff --git a/sdk-python/wave/sentiment.py b/sdk-python/wave_sdk/sentiment.py similarity index 100% rename from sdk-python/wave/sentiment.py rename to sdk-python/wave_sdk/sentiment.py diff --git a/sdk-python/wave/signage.py b/sdk-python/wave_sdk/signage.py similarity index 100% rename from sdk-python/wave/signage.py rename to sdk-python/wave_sdk/signage.py diff --git a/sdk-python/wave/slides.py b/sdk-python/wave_sdk/slides.py similarity index 100% rename from sdk-python/wave/slides.py rename to sdk-python/wave_sdk/slides.py diff --git a/sdk-python/wave/studio.py b/sdk-python/wave_sdk/studio.py similarity index 100% rename from sdk-python/wave/studio.py rename to sdk-python/wave_sdk/studio.py diff --git a/sdk-python/wave/studio_ai.py b/sdk-python/wave_sdk/studio_ai.py similarity index 100% rename from sdk-python/wave/studio_ai.py rename to sdk-python/wave_sdk/studio_ai.py diff --git a/sdk-python/wave/transcribe.py b/sdk-python/wave_sdk/transcribe.py similarity index 100% rename from sdk-python/wave/transcribe.py rename to sdk-python/wave_sdk/transcribe.py diff --git a/sdk-python/wave/usb.py b/sdk-python/wave_sdk/usb.py similarity index 100% rename from sdk-python/wave/usb.py rename to sdk-python/wave_sdk/usb.py diff --git a/sdk-python/wave/vault.py b/sdk-python/wave_sdk/vault.py similarity index 100% rename from sdk-python/wave/vault.py rename to sdk-python/wave_sdk/vault.py diff --git a/sdk-python/wave/voice.py b/sdk-python/wave_sdk/voice.py similarity index 100% rename from sdk-python/wave/voice.py rename to sdk-python/wave_sdk/voice.py diff --git a/sdk-python/wave/zoom.py b/sdk-python/wave_sdk/zoom.py similarity index 100% rename from sdk-python/wave/zoom.py rename to sdk-python/wave_sdk/zoom.py From ec40c942d9a827a2611c65d387eee0bdf93f533e Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Fri, 4 Sep 2026 15:55:34 -0400 Subject: [PATCH 2/4] fix(sdk-python): stage the actual renamed-module content (previous commit was stale) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `git mv sdk-python/wave sdk-python/wave_sdk` stages the rename immediately, with the PRE-edit content. The subsequent `sed` fixes to those files' internal imports (and to tests/test_sdk_exports.py) only touched the working tree. When re-staging, `git add` included a stale pathspec (`sdk-python/wave`, already gone after the mv) in the same invocation; git aborted the whole add atomically and left the index holding the git-mv-only (unfixed) content, which the prior commit then captured — confirmed by `git show :sdk-python/wave_sdk/__init__.py` still reading `from wave.client import ...` and CI's `test (3.12)` failing with `ImportError: cannot import name 'ClipsAPI' from 'wave'` (the stdlib module, since the internal imports never actually changed in the committed blob). This commit stages the real, already-verified-locally content: internal imports read `from wave_sdk.X import`, and tests/test_sdk_exports.py imports/asserts against `wave_sdk`. No source lines change from what the previous commit's message described; only what actually reached the index does. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Qb9cAaNZxep34EETf8ou9g --- sdk-python/tests/test_sdk_exports.py | 28 +++++----- sdk-python/wave_sdk/__init__.py | 78 ++++++++++++++-------------- sdk-python/wave_sdk/async_client.py | 4 +- sdk-python/wave_sdk/audience.py | 2 +- sdk-python/wave_sdk/captions.py | 2 +- sdk-python/wave_sdk/chapters.py | 2 +- sdk-python/wave_sdk/clips.py | 4 +- sdk-python/wave_sdk/collab.py | 2 +- sdk-python/wave_sdk/connect.py | 2 +- sdk-python/wave_sdk/creator.py | 2 +- sdk-python/wave_sdk/desktop.py | 2 +- sdk-python/wave_sdk/distribution.py | 2 +- sdk-python/wave_sdk/drm.py | 2 +- sdk-python/wave_sdk/edge.py | 2 +- sdk-python/wave_sdk/editor.py | 2 +- sdk-python/wave_sdk/fleet.py | 2 +- sdk-python/wave_sdk/ghost.py | 2 +- sdk-python/wave_sdk/marketplace.py | 2 +- sdk-python/wave_sdk/mesh.py | 2 +- sdk-python/wave_sdk/notifications.py | 2 +- sdk-python/wave_sdk/phone.py | 2 +- sdk-python/wave_sdk/pipeline.py | 2 +- sdk-python/wave_sdk/podcast.py | 2 +- sdk-python/wave_sdk/prism.py | 2 +- sdk-python/wave_sdk/pulse.py | 2 +- sdk-python/wave_sdk/qr.py | 2 +- sdk-python/wave_sdk/scene.py | 2 +- sdk-python/wave_sdk/search.py | 2 +- sdk-python/wave_sdk/sentiment.py | 2 +- sdk-python/wave_sdk/signage.py | 2 +- sdk-python/wave_sdk/slides.py | 2 +- sdk-python/wave_sdk/studio.py | 2 +- sdk-python/wave_sdk/studio_ai.py | 2 +- sdk-python/wave_sdk/transcribe.py | 2 +- sdk-python/wave_sdk/usb.py | 2 +- sdk-python/wave_sdk/vault.py | 2 +- sdk-python/wave_sdk/voice.py | 2 +- sdk-python/wave_sdk/zoom.py | 2 +- 38 files changed, 91 insertions(+), 91 deletions(-) diff --git a/sdk-python/tests/test_sdk_exports.py b/sdk-python/tests/test_sdk_exports.py index f0f03f9..c361032 100644 --- a/sdk-python/tests/test_sdk_exports.py +++ b/sdk-python/tests/test_sdk_exports.py @@ -9,8 +9,8 @@ def test_all_modules_import(): - """All 33 API modules should be importable from wave package.""" - from wave import ( + """All 33 API modules should be importable from the wave_sdk package.""" + from wave_sdk import ( ClipsAPI, EditorAPI, VoiceAPI, PhoneAPI, CollabAPI, CaptionsAPI, ChaptersAPI, StudioAIAPI, TranscribeAPI, SentimentAPI, SearchAPI, SceneAPI, @@ -29,7 +29,7 @@ def test_all_modules_import(): def test_wave_client_import(): """Core client classes should import.""" - from wave import WaveClient, WaveError, RateLimitError + from wave_sdk import WaveClient, WaveError, RateLimitError assert callable(WaveClient) assert issubclass(WaveError, Exception) assert issubclass(RateLimitError, WaveError) @@ -37,14 +37,14 @@ def test_wave_client_import(): def test_wave_client_requires_api_key(): """WaveClient should raise ValueError without api_key.""" - from wave import WaveClient + from wave_sdk import WaveClient with pytest.raises(ValueError, match="api_key"): WaveClient(api_key="") def test_wave_convenience_class(): """Wave class should instantiate with all 33 API modules.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") # Existing P3 @@ -98,7 +98,7 @@ def test_api_count(): added (it is 35 at time of carve), and a hardcoded exact count is brittle — it breaks on every legitimate addition. The >= floor still catches an accidental mass-removal. """ - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") api_attrs = [a for a in dir(w) if not a.startswith('_') and a != 'client'] assert len(api_attrs) >= 33, f"Expected >= 33 APIs, got {len(api_attrs)}: {api_attrs}" @@ -106,7 +106,7 @@ def test_api_count(): def test_pipeline_has_methods(): """PipelineAPI should have expected methods.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") for method in ['create', 'get', 'list', 'start', 'stop', 'get_health', 'wait_for_live']: assert hasattr(w.pipeline, method), f"PipelineAPI missing {method}" @@ -114,7 +114,7 @@ def test_pipeline_has_methods(): def test_prism_has_methods(): """PrismAPI should have expected methods.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") for method in ['create_device', 'start_device', 'stop_device', 'discover_sources', 'get_presets', 'set_preset', 'recall_preset']: assert hasattr(w.prism, method), f"PrismAPI missing {method}" @@ -122,21 +122,21 @@ def test_prism_has_methods(): def test_studio_has_methods(): """StudioAPI should have expected methods.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") for method in ['create', 'start', 'stop', 'add_source', 'activate_scene', 'transition', 'set_program', 'get_audio_mix']: assert hasattr(w.studio, method), f"StudioAPI missing {method}" def test_version(): - """SDK version should be 2.0.0.""" - import wave - assert wave.__version__ == "2.0.0" + """SDK version should match pyproject.toml (3.0.0).""" + import wave_sdk + assert wave_sdk.__version__ == "3.0.0" def test_all_exports(): """__all__ should contain all API classes.""" - import wave + import wave_sdk expected = [ "ClipsAPI", "EditorAPI", "VoiceAPI", "PhoneAPI", "CollabAPI", "CaptionsAPI", "ChaptersAPI", "StudioAIAPI", "TranscribeAPI", @@ -149,4 +149,4 @@ def test_all_exports(): "PodcastAPI", "SlidesAPI", "UsbAPI", ] for cls in expected: - assert cls in wave.__all__, f"{cls} missing from __all__" + assert cls in wave_sdk.__all__, f"{cls} missing from __all__" diff --git a/sdk-python/wave_sdk/__init__.py b/sdk-python/wave_sdk/__init__.py index a535d87..1a758f4 100644 --- a/sdk-python/wave_sdk/__init__.py +++ b/sdk-python/wave_sdk/__init__.py @@ -4,62 +4,62 @@ Official Python SDK for the WAVE API by WAVE Inc. Example: - >>> from wave import Wave + >>> from wave_sdk import Wave >>> client = Wave(api_key="your-api-key") >>> streams = client.pipeline.list() >>> clips = client.clips.list() """ -from wave.client import WaveClient, WaveError, RateLimitError +from wave_sdk.client import WaveClient, WaveError, RateLimitError # Existing P3 modules -from wave.clips import ClipsAPI -from wave.editor import EditorAPI -from wave.voice import VoiceAPI -from wave.phone import PhoneAPI -from wave.collab import CollabAPI -from wave.captions import CaptionsAPI -from wave.chapters import ChaptersAPI -from wave.studio_ai import StudioAIAPI -from wave.transcribe import TranscribeAPI -from wave.sentiment import SentimentAPI -from wave.search import SearchAPI -from wave.scene import SceneAPI +from wave_sdk.clips import ClipsAPI +from wave_sdk.editor import EditorAPI +from wave_sdk.voice import VoiceAPI +from wave_sdk.phone import PhoneAPI +from wave_sdk.collab import CollabAPI +from wave_sdk.captions import CaptionsAPI +from wave_sdk.chapters import ChaptersAPI +from wave_sdk.studio_ai import StudioAIAPI +from wave_sdk.transcribe import TranscribeAPI +from wave_sdk.sentiment import SentimentAPI +from wave_sdk.search import SearchAPI +from wave_sdk.scene import SceneAPI # P1 modules -from wave.pipeline import PipelineAPI -from wave.studio import StudioAPI +from wave_sdk.pipeline import PipelineAPI +from wave_sdk.studio import StudioAPI # P2 modules -from wave.fleet import FleetAPI -from wave.ghost import GhostAPI -from wave.mesh import MeshAPI -from wave.edge import EdgeAPI -from wave.pulse import PulseAPI -from wave.prism import PrismAPI -from wave.zoom import ZoomAPI +from wave_sdk.fleet import FleetAPI +from wave_sdk.ghost import GhostAPI +from wave_sdk.mesh import MeshAPI +from wave_sdk.edge import EdgeAPI +from wave_sdk.pulse import PulseAPI +from wave_sdk.prism import PrismAPI +from wave_sdk.zoom import ZoomAPI # P3 new modules -from wave.vault import VaultAPI -from wave.marketplace import MarketplaceAPI -from wave.connect import ConnectAPI -from wave.distribution import DistributionAPI -from wave.desktop import DesktopAPI -from wave.signage import SignageAPI -from wave.qr import QrAPI -from wave.audience import AudienceAPI -from wave.creator import CreatorAPI +from wave_sdk.vault import VaultAPI +from wave_sdk.marketplace import MarketplaceAPI +from wave_sdk.connect import ConnectAPI +from wave_sdk.distribution import DistributionAPI +from wave_sdk.desktop import DesktopAPI +from wave_sdk.signage import SignageAPI +from wave_sdk.qr import QrAPI +from wave_sdk.audience import AudienceAPI +from wave_sdk.creator import CreatorAPI # P4 modules -from wave.podcast import PodcastAPI -from wave.slides import SlidesAPI -from wave.usb import UsbAPI +from wave_sdk.podcast import PodcastAPI +from wave_sdk.slides import SlidesAPI +from wave_sdk.usb import UsbAPI # Cross-cutting -from wave.notifications import NotificationsAPI -from wave.drm import DrmAPI +from wave_sdk.notifications import NotificationsAPI +from wave_sdk.drm import DrmAPI -__version__ = "2.0.0" +__version__ = "3.0.0" __all__ = [ "Wave", "WaveClient", @@ -89,7 +89,7 @@ class Wave: Full WAVE SDK client with all APIs attached. Example: - >>> from wave import Wave + >>> from wave_sdk import Wave >>> wave = Wave(api_key="your-api-key", organization_id="org_123") >>> streams = wave.pipeline.list() >>> clips = wave.clips.list() diff --git a/sdk-python/wave_sdk/async_client.py b/sdk-python/wave_sdk/async_client.py index ff2d946..a436b67 100644 --- a/sdk-python/wave_sdk/async_client.py +++ b/sdk-python/wave_sdk/async_client.py @@ -5,7 +5,7 @@ Uses httpx.AsyncClient for non-blocking I/O. Example: - >>> from wave.async_client import AsyncWaveClient + >>> from wave_sdk.async_client import AsyncWaveClient >>> async with AsyncWaveClient(api_key="key") as client: ... streams = await client.get("/v1/streams") """ @@ -19,7 +19,7 @@ import httpx -from wave.client import WaveError, RateLimitError +from wave_sdk.client import WaveError, RateLimitError logger = logging.getLogger("wave") diff --git a/sdk-python/wave_sdk/audience.py b/sdk-python/wave_sdk/audience.py index 470725e..bb872b8 100644 --- a/sdk-python/wave_sdk/audience.py +++ b/sdk-python/wave_sdk/audience.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Poll(BaseModel): id: str; stream_id: str; question: str; options: list[dict]; status: str; total_votes: int = 0; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/captions.py b/sdk-python/wave_sdk/captions.py index 10f4bdf..0ee1bf0 100644 --- a/sdk-python/wave_sdk/captions.py +++ b/sdk-python/wave_sdk/captions.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class CaptionTrack(BaseModel): id: str; asset_id: str; language: str; status: str; format: str; cue_count: int = 0; download_url: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/chapters.py b/sdk-python/wave_sdk/chapters.py index 876ae35..8b68880 100644 --- a/sdk-python/wave_sdk/chapters.py +++ b/sdk-python/wave_sdk/chapters.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Chapter(BaseModel): id: str; title: str; start_time: float; end_time: float; thumbnail_url: str | None = None; description: str | None = None diff --git a/sdk-python/wave_sdk/clips.py b/sdk-python/wave_sdk/clips.py index 66f014f..e83c13f 100644 --- a/sdk-python/wave_sdk/clips.py +++ b/sdk-python/wave_sdk/clips.py @@ -10,7 +10,7 @@ from typing import Any, Literal from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class ClipSource(BaseModel): @@ -78,7 +78,7 @@ class ClipsAPI: Clips API client. Example: - >>> from wave import Wave + >>> from wave_sdk import Wave >>> wave = Wave(api_key="your-api-key") >>> clip = wave.clips.create( ... title="Best Moment", diff --git a/sdk-python/wave_sdk/collab.py b/sdk-python/wave_sdk/collab.py index 2ab2bf4..18c4196 100644 --- a/sdk-python/wave_sdk/collab.py +++ b/sdk-python/wave_sdk/collab.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class CollabRoom(BaseModel): id: str; organization_id: str; name: str; status: str; participant_count: int = 0; max_participants: int = 50; settings: dict | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/connect.py b/sdk-python/wave_sdk/connect.py index fa0c164..b54933e 100644 --- a/sdk-python/wave_sdk/connect.py +++ b/sdk-python/wave_sdk/connect.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Integration(BaseModel): id: str; organization_id: str; name: str; type: str; provider: str; status: str; scopes: list[str] | None = None; last_sync_at: str | None = None; error_message: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/creator.py b/sdk-python/wave_sdk/creator.py index 948ab29..7aabd3b 100644 --- a/sdk-python/wave_sdk/creator.py +++ b/sdk-python/wave_sdk/creator.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class CreatorProfile(BaseModel): id: str; user_id: str; display_name: str; subscriber_count: int = 0; total_revenue_cents: int = 0; verified: bool = False; tier: str = "starter"; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/desktop.py b/sdk-python/wave_sdk/desktop.py index 6d85fa0..08acc75 100644 --- a/sdk-python/wave_sdk/desktop.py +++ b/sdk-python/wave_sdk/desktop.py @@ -1,7 +1,7 @@ """WAVE SDK - Desktop API. Desktop Node application management.""" from __future__ import annotations from typing import Any -from wave.client import WaveClient +from wave_sdk.client import WaveClient class DesktopAPI: def __init__(self, client: WaveClient): self._client = client; self._base = "/v1/desktop" diff --git a/sdk-python/wave_sdk/distribution.py b/sdk-python/wave_sdk/distribution.py index 50193d9..ed58227 100644 --- a/sdk-python/wave_sdk/distribution.py +++ b/sdk-python/wave_sdk/distribution.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Destination(BaseModel): id: str; organization_id: str; name: str; type: str; status: str; auto_start: bool = False; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/drm.py b/sdk-python/wave_sdk/drm.py index 94534bf..aff7cc5 100644 --- a/sdk-python/wave_sdk/drm.py +++ b/sdk-python/wave_sdk/drm.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class DRMPolicy(BaseModel): id: str; organization_id: str; name: str; providers: list[str]; allow_offline: bool = False; max_devices: int = 1; output_protection: str = "none"; persistent_license: bool = False; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/edge.py b/sdk-python/wave_sdk/edge.py index e9dab44..79d586b 100644 --- a/sdk-python/wave_sdk/edge.py +++ b/sdk-python/wave_sdk/edge.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class EdgeNode(BaseModel): id: str; name: str; region: str; provider: str; status: str; latency_ms: int; capacity_percent: float; active_workers: int; bandwidth_mbps: float; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/editor.py b/sdk-python/wave_sdk/editor.py index b31bb92..84e9339 100644 --- a/sdk-python/wave_sdk/editor.py +++ b/sdk-python/wave_sdk/editor.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class EditorProject(BaseModel): id: str; organization_id: str; title: str; status: str; duration: float = 0; track_count: int = 0; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/fleet.py b/sdk-python/wave_sdk/fleet.py index ca7c85f..dd7e8de 100644 --- a/sdk-python/wave_sdk/fleet.py +++ b/sdk-python/wave_sdk/fleet.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class FleetNode(BaseModel): id: str; organization_id: str; name: str; status: str; health: str; ip_address: str | None = None; version: str | None = None; os: str | None = None; cpu_usage: float = 0; memory_usage: float = 0; device_count: int = 0; last_seen_at: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/ghost.py b/sdk-python/wave_sdk/ghost.py index c2960f9..feee4aa 100644 --- a/sdk-python/wave_sdk/ghost.py +++ b/sdk-python/wave_sdk/ghost.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class GhostSession(BaseModel): id: str; production_id: str; mode: str; style: str; status: str; confidence_threshold: float = 0.7; created_at: str | None = None diff --git a/sdk-python/wave_sdk/marketplace.py b/sdk-python/wave_sdk/marketplace.py index 9ed724c..66c03b8 100644 --- a/sdk-python/wave_sdk/marketplace.py +++ b/sdk-python/wave_sdk/marketplace.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class MarketplaceItem(BaseModel): id: str; name: str; description: str; type: str; status: str; author_name: str; version: str; price_cents: int = 0; downloads: int = 0; rating: float = 0; tags: list[str] | None = None; category: str; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/mesh.py b/sdk-python/wave_sdk/mesh.py index ab447f7..54ebde9 100644 --- a/sdk-python/wave_sdk/mesh.py +++ b/sdk-python/wave_sdk/mesh.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class MeshRegion(BaseModel): id: str; name: str; provider: str; location: str; status: str; latency_ms: int; capacity_percent: float; stream_count: int; viewer_count: int; is_primary: bool; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/notifications.py b/sdk-python/wave_sdk/notifications.py index 6a2db29..039a8d3 100644 --- a/sdk-python/wave_sdk/notifications.py +++ b/sdk-python/wave_sdk/notifications.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Notification(BaseModel): id: str; user_id: str; type: str; title: str; body: str; status: str; priority: str; channel: str; action_url: str | None = None; read_at: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/phone.py b/sdk-python/wave_sdk/phone.py index 6f2d79e..822d792 100644 --- a/sdk-python/wave_sdk/phone.py +++ b/sdk-python/wave_sdk/phone.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class PhoneNumber(BaseModel): id: str; number: str; type: str; capabilities: list[str] | None = None; status: str; region: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/pipeline.py b/sdk-python/wave_sdk/pipeline.py index f3afcbd..56452d5 100644 --- a/sdk-python/wave_sdk/pipeline.py +++ b/sdk-python/wave_sdk/pipeline.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Stream(BaseModel): id: str; organization_id: str; title: str; description: str | None = None; status: str; protocol: str; ingest_url: str | None = None; playback_url: str | None = None; stream_key: str | None = None; resolution: str | None = None; frame_rate: int | None = None; bitrate_kbps: int | None = None; viewer_count: int = 0; recording_enabled: bool = False; tags: list[str] | None = None; metadata: dict[str, Any] | None = None; started_at: str | None = None; ended_at: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/podcast.py b/sdk-python/wave_sdk/podcast.py index 8577da2..2864209 100644 --- a/sdk-python/wave_sdk/podcast.py +++ b/sdk-python/wave_sdk/podcast.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Podcast(BaseModel): id: str; organization_id: str; title: str; description: str; category: str; language: str = "en"; episode_count: int = 0; subscriber_count: int = 0; rss_url: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/prism.py b/sdk-python/wave_sdk/prism.py index 6c4c42d..e5d6cd5 100644 --- a/sdk-python/wave_sdk/prism.py +++ b/sdk-python/wave_sdk/prism.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class VirtualDevice(BaseModel): id: str; organization_id: str; name: str; type: str; status: str; source_protocol: str; source_endpoint: str; node_id: str; resolution: dict | None = None; frame_rate: int | None = None; health_score: float = 100; ptz_enabled: bool = False; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/pulse.py b/sdk-python/wave_sdk/pulse.py index 7562bb2..8b7985a 100644 --- a/sdk-python/wave_sdk/pulse.py +++ b/sdk-python/wave_sdk/pulse.py @@ -1,7 +1,7 @@ """WAVE SDK - Pulse Analytics API. Streaming analytics and BI.""" from __future__ import annotations from typing import Any -from wave.client import WaveClient +from wave_sdk.client import WaveClient class PulseAPI: def __init__(self, client: WaveClient): self._client = client; self._base = "/v1/analytics" diff --git a/sdk-python/wave_sdk/qr.py b/sdk-python/wave_sdk/qr.py index e776e40..1bd47f1 100644 --- a/sdk-python/wave_sdk/qr.py +++ b/sdk-python/wave_sdk/qr.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class QRCode(BaseModel): id: str; organization_id: str; type: str; content: str; short_url: str; image_url: str; scan_count: int = 0; status: str = "active"; expires_at: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/scene.py b/sdk-python/wave_sdk/scene.py index 5d32a6c..2a4fd30 100644 --- a/sdk-python/wave_sdk/scene.py +++ b/sdk-python/wave_sdk/scene.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Scene(BaseModel): id: str; start_time: float; end_time: float; scene_type: str; shot_type: str | None = None; confidence: float; labels: list[str] | None = None; thumbnail_url: str | None = None diff --git a/sdk-python/wave_sdk/search.py b/sdk-python/wave_sdk/search.py index f453691..9faa2a0 100644 --- a/sdk-python/wave_sdk/search.py +++ b/sdk-python/wave_sdk/search.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class SearchResult(BaseModel): id: str; type: str; title: str; score: float; highlights: list[dict] | None = None; thumbnail_url: str | None = None; created_at: str diff --git a/sdk-python/wave_sdk/sentiment.py b/sdk-python/wave_sdk/sentiment.py index 0135642..ab440ba 100644 --- a/sdk-python/wave_sdk/sentiment.py +++ b/sdk-python/wave_sdk/sentiment.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class SentimentAnalysis(BaseModel): id: str; asset_id: str; status: str; overall_sentiment: str | None = None; overall_score: float = 0; segments_count: int = 0; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/signage.py b/sdk-python/wave_sdk/signage.py index 0f6ffe6..f604b1d 100644 --- a/sdk-python/wave_sdk/signage.py +++ b/sdk-python/wave_sdk/signage.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Display(BaseModel): id: str; organization_id: str; name: str; status: str; resolution: str | None = None; orientation: str = "landscape"; location: str | None = None; current_playlist_id: str | None = None; last_seen_at: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/slides.py b/sdk-python/wave_sdk/slides.py index 8b70486..da1c577 100644 --- a/sdk-python/wave_sdk/slides.py +++ b/sdk-python/wave_sdk/slides.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Conversion(BaseModel): id: str; organization_id: str; title: str; status: str; input_format: str; input_url: str; output_url: str | None = None; slide_count: int = 0; duration_seconds: float | None = None; progress_percent: int = 0; error: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/studio.py b/sdk-python/wave_sdk/studio.py index 0ba7964..e374814 100644 --- a/sdk-python/wave_sdk/studio.py +++ b/sdk-python/wave_sdk/studio.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Production(BaseModel): id: str; organization_id: str; title: str; description: str | None = None; status: str; program_source_id: str | None = None; preview_source_id: str | None = None; active_scene_id: str | None = None; recording_enabled: bool = False; streaming_enabled: bool = False; viewer_count: int = 0; started_at: str | None = None; ended_at: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/studio_ai.py b/sdk-python/wave_sdk/studio_ai.py index 997bf4f..d3abc4e 100644 --- a/sdk-python/wave_sdk/studio_ai.py +++ b/sdk-python/wave_sdk/studio_ai.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class AIAssistant(BaseModel): id: str; production_id: str | None = None; stream_id: str | None = None; mode: str; status: str; config: dict | None = None; stats: dict | None = None; started_at: str | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/transcribe.py b/sdk-python/wave_sdk/transcribe.py index f86a1ff..05d9229 100644 --- a/sdk-python/wave_sdk/transcribe.py +++ b/sdk-python/wave_sdk/transcribe.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Speaker(BaseModel): id: str; label: str; segments_count: int = 0; total_duration: float = 0 diff --git a/sdk-python/wave_sdk/usb.py b/sdk-python/wave_sdk/usb.py index bbf6c25..aabf40d 100644 --- a/sdk-python/wave_sdk/usb.py +++ b/sdk-python/wave_sdk/usb.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class USBDevice(BaseModel): id: str; node_id: str; name: str; vendor_id: str; product_id: str; device_class: str; status: str; manufacturer: str | None = None; speed: str; capabilities: list[str] | None = None; connected_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/vault.py b/sdk-python/wave_sdk/vault.py index 0af3416..fa2ee2d 100644 --- a/sdk-python/wave_sdk/vault.py +++ b/sdk-python/wave_sdk/vault.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Recording(BaseModel): id: str; organization_id: str; stream_id: str | None = None; title: str; status: str; duration_seconds: float = 0; file_size_bytes: int = 0; format: str | None = None; storage_tier: str = "hot"; playback_url: str | None = None; download_url: str | None = None; tags: list[str] | None = None; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/voice.py b/sdk-python/wave_sdk/voice.py index 6986e5b..92361f7 100644 --- a/sdk-python/wave_sdk/voice.py +++ b/sdk-python/wave_sdk/voice.py @@ -3,7 +3,7 @@ import time from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class Voice(BaseModel): id: str; name: str; language: str; gender: str | None = None; model_type: str; preview_url: str | None = None; is_custom: bool = False; created_at: str; updated_at: str diff --git a/sdk-python/wave_sdk/zoom.py b/sdk-python/wave_sdk/zoom.py index de128c2..acc4263 100644 --- a/sdk-python/wave_sdk/zoom.py +++ b/sdk-python/wave_sdk/zoom.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any from pydantic import BaseModel -from wave.client import WaveClient +from wave_sdk.client import WaveClient class ZoomMeeting(BaseModel): id: str; topic: str; type: str; status: str; start_url: str; join_url: str; host_id: str; duration_minutes: int; participants_count: int = 0; recording_enabled: bool = False; rtms_enabled: bool = False; created_at: str From b6cc6399b5d41f7b2d6e6270bbcfa241b07683d8 Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Fri, 4 Sep 2026 15:58:28 -0400 Subject: [PATCH 3/4] fix(ga): actually stage the gate-arming + docs changes (also lost to the bad-pathspec add) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same root cause as the previous commit: the first `git add` invocation included a pathspec that matched nothing (`sdk-python/wave`, already gone after `git mv`), which aborts the WHOLE `git add` atomically and leaves the index untouched for every path in that call — not just the renamed ones. This silently dropped the actual gate-arming change: .github/workflows/registry-cleanroom.yml (removing the pull_request informational-only branch — the core of deliverable 2), GA-READINESS.md (the "Arming window" section), sdk-python/CHANGELOG.md, sdk-python/README.md, sdk-python/pyproject.toml (name/version/packages.find), and sdk-python/tests/conftest.py. Confirmed via `gh run view` on this PR's own `cleanroom` check (run 33913649979, against commit ec40c94): the log still showed the OLD "Report (pull request, informational)" step and its `::warning`-then-exit-0 behavior, proving the workflow file at that commit was still the pre-fix version despite the PR body describing the fix as present. Staged this time with one `git add -- ` per file (no bad pathspec in the same invocation) and verified `git diff --cached` before committing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Qb9cAaNZxep34EETf8ou9g --- .github/workflows/registry-cleanroom.yml | 57 ++++++++++++++++-------- GA-READINESS.md | 54 ++++++++++++++++++---- sdk-python/CHANGELOG.md | 24 ++++++++++ sdk-python/README.md | 4 +- sdk-python/pyproject.toml | 11 ++++- sdk-python/tests/conftest.py | 2 +- 6 files changed, 120 insertions(+), 32 deletions(-) diff --git a/.github/workflows/registry-cleanroom.yml b/.github/workflows/registry-cleanroom.yml index e06b0f8..43509e5 100644 --- a/.github/workflows/registry-cleanroom.yml +++ b/.github/workflows/registry-cleanroom.yml @@ -14,14 +14,33 @@ name: registry clean-room acceptance # range on @wave-av/sdk is a caret range, so what a customer receives is decided by npm's resolver # on the day they install, not by anything in this repo. Only a scheduled run catches that. # -# BLOCKING BEHAVIOUR -# schedule / workflow_run(release) / workflow_dispatch -> HARD FAIL, and open-or-update an issue. -# pull_request -> informational only. -# A PR is not the cause of an already-published defect, so a PR is not blocked by one. The PR run -# exists so that a change to the gate itself is exercised before it merges. There is deliberately -# NO path filter: the check reports on every PR, which is what makes it eligible to become a -# required status check later (a path-filtered required check stays permanently unreported and -# blocks every PR that misses the filter — the lesson already recorded in registry-parity.yml). +# BLOCKING BEHAVIOUR (revised 2026-09-04, GA false-green remediation) +# Every trigger — schedule / workflow_run(release) / workflow_dispatch / pull_request — HARD +# FAILS the job (non-zero exit) when a published artifact fails clean-room acceptance. +# schedule / workflow_run / workflow_dispatch additionally open-or-update a tracking issue, +# because a nightly that fails quietly is worse than no nightly at all. +# +# Until 2026-09-04 pull_request ran the identical checks but always exited 0 — a `::warning` +# annotation stood in for a real result, so the GitHub check CONCLUSION read SUCCESS even when +# the run's own log ended "REGISTRY CLEAN-ROOM FAILED: 8 check(s)". sdks#79 merged on the +# strength of that green rollup, carrying the 8 known artifact defects onto main un-remediated. +# That is now impossible: this job reports its true state on every event. +# +# WHY IT IS NOT (yet) A *REQUIRED* STATUS CHECK +# Failing honestly and being required to merge are two different questions. As of 2026-09-04 +# this repo's own published artifacts, plus artifacts published from wave-av/cli and +# wave-av/sdk-python that this gate also tests, carry known defects (see GA-READINESS.md, +# "Arming window") that cannot be cleared by an sdks PR alone — some require a publish this +# workflow cannot make (npm/PyPI publish is operator-gated), some require a fix landing in a +# different repo. Making this check *required* today would red every future sdks PR for a +# defect that PR did not introduce and cannot fix — training reviewers to override or ignore a +# required check is worse than not having one. So: loud-but-advisory now (this job fails +# honestly, and that failure is visible on statusCheckRollup), required later, once +# GA-READINESS.md's "Arming window" section reports zero outstanding cleanroom failures. There +# is deliberately NO path filter: the check reports on every PR, which is what makes it eligible +# to become a required status check later without a permanently-unreported gap (a path-filtered +# required check blocks every PR that misses the filter — the lesson already recorded in +# registry-parity.yml). on: schedule: @@ -134,9 +153,17 @@ jobs: fi - name: Enforce - # Hard-fail everywhere the result is actionable. A pull request is informational: it did not - # publish the artifact under test and cannot fix it. - if: github.event_name != 'pull_request' + # Hard-fail on every trigger, including pull_request. A PR did not publish the artifact + # under test and cannot fix it by itself — but that is an argument against making this a + # *required* branch-protection check while known registry defects are outstanding + # (tracked separately; see GA-READINESS.md "Arming window"), never an argument for + # reporting a false SUCCESS. Before 2026-09-04 this step no-opped on pull_request and a + # sibling "informational" step printed a ::warning while still exiting 0 — so the check + # conclusion read SUCCESS on a PR whose own log said "REGISTRY CLEAN-ROOM FAILED: 8 + # check(s)" (sdks#79, merged 2026-09-04T18:25:54Z on the strength of that green rollup). + # A gate that cannot fail is not a gate. This step now fails loud on every event; the + # only thing pull_request changes is whether GitHub treats that failure as *required*, + # and that is controlled by branch protection, not by this workflow. env: CODE: ${{ steps.cleanroom.outputs.exit_code }} run: | @@ -145,11 +172,3 @@ jobs: exit 1 fi echo "registry clean-room acceptance passed" - - - name: Report (pull request, informational) - if: github.event_name == 'pull_request' - env: - CODE: ${{ steps.cleanroom.outputs.exit_code }} - run: | - [ "$CODE" = "0" ] && echo "clean-room acceptance passed" \ - || echo "::warning title=registry clean-room::published artifacts fail clean-room acceptance (exit $CODE). Not blocking this PR — see the job summary." diff --git a/GA-READINESS.md b/GA-READINESS.md index 2b4b047..024db73 100644 --- a/GA-READINESS.md +++ b/GA-READINESS.md @@ -150,15 +150,53 @@ notes: | branch-protection attestation. ``` +## Arming window (2026-09-04 false-green remediation) + +`registry-cleanroom.yml`'s `pull_request` trigger used to run the same checks and then paper +over a failure with a `::warning` while still exiting `0` — so the GitHub check conclusion read +**SUCCESS** on a PR whose own log ended `REGISTRY CLEAN-ROOM FAILED: 8 check(s)`. sdks#79 merged +2026-09-04T18:25:54Z on the strength of that green rollup. As of this commit, every trigger +(including `pull_request`) hard-fails the job when a check fails — the gate can no longer report +green while failing. + +It is **not yet a required branch-protection status check**, deliberately. `cleanroom` also +tests artifacts published from **two other repositories** — `wave-av/cli` (`@wave-av/cli`) and +`wave-av/sdk-python` (`wave-sdk` on PyPI) — that an `sdks` PR cannot fix by itself. Requiring it +today would red every future `sdks` PR for a defect it did not introduce, training reviewers to +override rather than read it. Sequence: + +1. This repo's own defect (`wave-av-sdk`'s `wave`/stdlib collision, ART-001) is fixed **in + source** in this same change (`wave/` renamed to `wave_sdk/`, version bumped to `3.0.0`) but + remains **unpublished** — publishing to PyPI is an operator-gated action (Trusted Publisher + registration + the `pypi-publish` environment's required reviewer), not something this lane + may cross. `cleanroom` will keep reporting `wave-av-sdk py-import-module` / + `py-no-stdlib-shadow` as FAIL against the live registry until that publish happens — correctly, + because it tests what PyPI serves, never this checkout. +2. `wave-av/cli` fixes and republishes with: `wave --version` reading from its own + `package.json` (not a stale constant), `@wave-av/sdk` pinned exact rather than `^2.0.11`, and + npm provenance attestation present. +3. `wave-av/sdk-python` fixes the same stdlib-collision defect in `wave-sdk` (that package is a + distinct source tree from this repo's `wave-av-sdk` — see `CHANGELOG.md`'s note — so this + commit does not touch it). +4. Once `node scripts/ga/registry-cleanroom.mjs` reports **zero** failing checks against the + live registries, add `registry clean-room acceptance / cleanroom` to the default branch's + required status checks. It already runs on every PR with no path filter, so it can be made + required without a permanently-unreported gap. + +`@wave-av/mcp-server`'s `serverInfo.version` mismatch (0.1.0 vs 0.2.0), one of the original 8 +failures, is **already resolved**: the live registry now serves `@wave-av/mcp-server@0.2.1` with +`serverInfo.version` `0.2.1` (re-verified 2026-09-04 against `registry.npmjs.org` and the +package's own stdio `initialize` response — see the clean-room run below). 7 checks remain +failing, all pre-existing and none introduced by this change: 3 in `wave-av/cli`, 2 in +`wave-av/sdk-python`, 2 (`wave-av-sdk` import + stdlib-shadow) fixed in source here and pending +publish. + ## Operator actions to finish these criteria -1. **Make the release gate blocking.** The clean-room job hard-fails on schedule, release and - dispatch, but it is not yet a *required* status check. Add - `registry clean-room acceptance / cleanroom` to the default branch's required checks. It runs on - every pull request with no path filter precisely so it can be made required without going - permanently unreported. -2. **Fix the three artifact defects the gate found** (each needs a publish, which is a named floor - and not this lane's to cross): the CLI version constant, the MCP server's `serverInfo.version`, - and the Python distribution's top-level module name. +1. **Make the release gate blocking** once the arming window above closes (all 7 checks pass). +2. **Fix the remaining artifact defects the gate found** (each needs a publish, which is a named + floor and not this lane's to cross): the CLI version constant and dependency pin live in + `wave-av/cli`; the Python distribution's top-level module name for `wave-sdk` lives in + `wave-av/sdk-python`. `wave-av-sdk`'s equivalent is fixed in source in this commit. 3. **Republish the CLI through the provenance-emitting workflow** so `dist.attestations` is populated, and exact-pin its first-party dependency. diff --git a/sdk-python/CHANGELOG.md b/sdk-python/CHANGELOG.md index b75b656..90a6b75 100644 --- a/sdk-python/CHANGELOG.md +++ b/sdk-python/CHANGELOG.md @@ -5,6 +5,30 @@ > *different* package published from a different repository (`wave-av/sdk-python`). The two > are not the same distribution and their version numbers are unrelated. +## 3.0.0 (2026-09-04) + +### Fixed (ART-001 — GA registry clean-room acceptance) + +- **The importable top-level module is now `wave_sdk`, not `wave`.** Every release through + `2.0.1` shipped a distribution whose only top-level name was `wave`, which collides with the + CPython stdlib module of the same name (`wave` ∈ `sys.stdlib_module_names`). Because the + stdlib directory precedes site-packages on `sys.path`, `import wave` in a real installation + always resolved to the stdlib WAV-file reader, never to this SDK — the documented + `from wave import Wave` entry point could not succeed for any customer. `scripts/ga/ + registry-cleanroom.mjs` in `wave-av/sdks` proved this against the live PyPI artifact + (`py-no-stdlib-shadow` / `py-import-module`, both FAIL on `wave-av-sdk@2.0.0`). + Renamed `wave/` -> `wave_sdk/` and every internal absolute import + (`from wave.X import` -> `from wave_sdk.X import`). Public API is otherwise unchanged + (`from wave_sdk import Wave`). This is a correctness fix, not a break of a working + integration, and the major version bump reflects the import-path change, not a functional + regression — there is no prior version where the old import path actually worked. +- This fix is **source-only in this commit**. It reaches PyPI only on the next + `sdk-python-v*` tag publish (`.github/workflows/publish-pypi.yml`), which is an + operator-gated action (PyPI Trusted Publisher + `pypi-publish` environment + required-reviewer). Until that publish happens, `registry-cleanroom` will continue to report + `wave-av-sdk@2.0.0 py-import-module` / `py-no-stdlib-shadow` as FAIL, correctly, because it + tests what PyPI actually serves — not this checkout. + ## 2.0.1 (2026-09-03) ### Fixed diff --git a/sdk-python/README.md b/sdk-python/README.md index 84c49ab..ab505a4 100644 --- a/sdk-python/README.md +++ b/sdk-python/README.md @@ -11,7 +11,7 @@ pip install wave-av-sdk ## Quick start ```python -from wave import Wave +from wave_sdk import Wave wave = Wave(api_key="your-api-key", organization_id="org_123") @@ -93,7 +93,7 @@ viewers = wave.pulse.get_viewer_analytics(time_range="24h") ## Error handling ```python -from wave import WaveError, RateLimitError +from wave_sdk import WaveError, RateLimitError try: wave.pipeline.get("invalid-id") diff --git a/sdk-python/pyproject.toml b/sdk-python/pyproject.toml index d836b86..b81f9eb 100644 --- a/sdk-python/pyproject.toml +++ b/sdk-python/pyproject.toml @@ -7,7 +7,14 @@ name = "wave-av-sdk" # 2.0.0 is already on PyPI, published as MIT, and PyPI releases are immutable. The Apache-2.0 # licensing this file declares can therefore only reach a user at a NEW version — see # scripts/registry_license_truth.py, which fails the build if this ever collides again. -version = "2.0.1" +# +# 3.0.0: the importable top-level module was renamed `wave` -> `wave_sdk` (ART-001/GA-2026-09-04). +# `wave` collides with the CPython stdlib module of the same name, and because the stdlib +# directory precedes site-packages on sys.path, `import wave` always resolved to the stdlib WAV +# reader, never to this SDK, on every published version through 2.0.1. The documented +# `from wave import Wave` entry point could never succeed for a real installer — this is a +# correctness fix, not a break of a working integration, and it is why the major version moves. +version = "3.0.0" description = "Official WAVE SDK for Python - 33 API modules for streaming, production, analytics, and more" readme = "README.md" license = {text = "Apache-2.0"} @@ -74,7 +81,7 @@ Issues = "https://github.com/wave-av/sdks/issues" [tool.setuptools.packages.find] where = ["."] -include = ["wave*"] +include = ["wave_sdk*"] [tool.mypy] python_version = "3.10" diff --git a/sdk-python/tests/conftest.py b/sdk-python/tests/conftest.py index 417a394..7c1e788 100644 --- a/sdk-python/tests/conftest.py +++ b/sdk-python/tests/conftest.py @@ -9,5 +9,5 @@ def api_key(): @pytest.fixture def wave_client(): - from wave import Wave + from wave_sdk import Wave return Wave(api_key="test-api-key", organization_id="org_test") From 965ed8e825fa85be11487b496c16db1f311ee9d4 Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Fri, 4 Sep 2026 16:14:02 -0400 Subject: [PATCH 4/4] fix(ga): harden mcp-server's serverInfo.version against regression; correct GA-READINESS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mcp-serverinfo-version-matches-package (one of PR #79's original 8 failures) now passes against the live registry (@wave-av/mcp-server@0.2.1 self-resolved via an independent publish), but src/server.ts on origin/main still hardcoded `version: "0.1.0"` in the McpServer constructor — the exact defect class that caused the original failure. Building and publishing from unmodified main would have reintroduced it verbatim. Add src/version.ts (mirrors wave-av/cli's src/lib/version.ts pattern: walk up from the module's own location to the nearest package.json, verify its name matches this package, read version) and wire MCP_SERVER_VERSION into server.ts instead of the literal. Add __tests__/version.test.ts as a VER-001 regression guard. mcp-server was also the only package in this workspace missing a `test` script + vitest devDependency; added both. Verified: tsc --noEmit clean; vitest run __tests__/version.test.ts 2/2 pass; built dist/index.js and probed it live over stdio JSON-RPC — serverInfo.version now reads 0.1.8 (this package's actual version), not a literal. Also correct GA-READINESS.md: re-verified against wave-av/cli and wave-av/sdk-python (separate repos, both public) that the other 6 originally-failing checks are ALSO already fixed in source there (wave-av/cli commit 91093d5 derives CLI_VERSION from package.json and pins @wave-av/sdk exact; release.yml already runs npm publish --provenance; wave-av/sdk-python's origin/main already ships wave_sdk/ at 2.1.0) — all 8 are root-cause fixed across three repos, none is an open defect, all 7 still-failing live checks are blocked only on an operator-gated publish this lane may not cross. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Qb9cAaNZxep34EETf8ou9g --- GA-READINESS.md | 152 ++++++++++-------- .../mcp-server/__tests__/version.test.ts | 34 ++++ .../packages/mcp-server/package.json | 4 +- .../packages/mcp-server/src/server.ts | 3 +- .../packages/mcp-server/src/version.ts | 55 +++++++ sdk-typescript/pnpm-lock.yaml | 51 ++++++ 6 files changed, 226 insertions(+), 73 deletions(-) create mode 100644 sdk-typescript/packages/mcp-server/__tests__/version.test.ts create mode 100644 sdk-typescript/packages/mcp-server/src/version.ts diff --git a/GA-READINESS.md b/GA-READINESS.md index 024db73..c09f61c 100644 --- a/GA-READINESS.md +++ b/GA-READINESS.md @@ -7,10 +7,10 @@ ```yaml repository: wave-av/sdks -revision: 70b2a04a205658a25b3723adec9faf665e204af6 +revision: 61e473c627866e06f7474e843d2a077ae67f76e2 # origin/main tip this branch is based on repo_class: sdk owner: WAVE platform / SDK publishing -last_evaluated_at: 2026-09-04T00:31:56Z +last_evaluated_at: 2026-09-04T19:59:00Z evaluator_version: scripts/ga/registry-cleanroom.mjs spec_version: 1.0.0 overall_status: fail @@ -69,29 +69,32 @@ no claim about them. Absence here is `unknown` at the platform gate, not `not_ap criterion_id: ART-001 status: fail owner: WAVE platform / SDK publishing -verification_command: node scripts/ga/registry-cleanroom.mjs -verified_revision: 70b2a04a205658a25b3723adec9faf665e204af6 -verified_at: 2026-09-04T00:31:56Z +verification_command: node scripts/ga/registry-cleanroom.mjs --python python3.12 +verified_revision: 61e473c627866e06f7474e843d2a077ae67f76e2 +verified_at: 2026-09-04T19:59:00Z evidence: - uri: ci://wave-av/sdks/.github/workflows/registry-cleanroom.yml#cleanroom-report.json - sha256: 0f03603df2bc33f50958a75c8998777463507c1f9e3ee0fec811d82a3ca1206d + sha256: ddb12dfc51476c1d4406806fac37f096474c68a698e41fce1a5815767ba8f10d pass_condition_from_spec: > All supported runtimes pass from public registries; Python uses a non-stdlib-colliding import; source, package metadata, CLI banner, tag and GitHub release agree. notes: | - Observed against the live registries on 2026-09-04. Passing: + Re-observed against the live registries on 2026-09-04, re-running the same command that + produced the original 8-failure PR #79 run. One of the 8 originally-failing checks + (`@wave-av/mcp-server` `mcp-serverinfo-version-matches-package`) has SELF-RESOLVED live — an + npm publish of 0.2.1 landed independently of this change — but its SOURCE on `origin/main` + still hardcoded the version literal that caused the original defect (a live pass with a latent + regression). This change hardens that source (see "Arming window" below). 7 checks remain + failing against the live registries, ALL already fixed in source somewhere (table below) and + blocked only on an operator-gated publish: @wave-av/sdk@2.1.3 ESM import, CJS require, all 46 declared subpath exports resolve. @wave-av/adk@1.0.15 installs and imports. - @wave-av/mcp-server@0.2.0 starts over stdio, lists 18 tools, serves every tool its shipped - README advertises. - Failing: - @wave-av/cli@1.0.8 `wave --version` prints 1.0.0. Installs cleanly and `--help` exits 0, - so nothing short of running the binary detects this. - wave-sdk@2.0.0 (PyPI) `from wave_sdk import Wave` raises ModuleNotFoundError. The wheel's - only top-level name is `wave`, which collides with the CPython stdlib - module of that name; because the stdlib directory precedes - site-packages, `import wave` returns the stdlib WAV reader and the SDK - is unreachable by any name. The artifact is unusable as published. + @wave-av/mcp-server@0.2.1 starts over stdio, lists 18 tools, serves every tool its shipped + README advertises, AND serverInfo.version now correctly matches. + Failing (root cause fixed in source, publish pending — see the arming-window table below): + @wave-av/cli@1.0.8 `wave --version` prints 1.0.0. + wave-sdk@2.0.0 (PyPI) `from wave_sdk import Wave` raises ModuleNotFoundError — top-level + name `wave` collides with the CPython stdlib module of that name. wave-av-sdk@2.0.0 (PyPI) identical defect. ``` @@ -99,23 +102,29 @@ notes: | criterion_id: VER-001 status: fail owner: WAVE platform / SDK publishing -verification_command: node scripts/ga/registry-cleanroom.mjs -verified_revision: 70b2a04a205658a25b3723adec9faf665e204af6 -verified_at: 2026-09-04T00:31:56Z +verification_command: node scripts/ga/registry-cleanroom.mjs --python python3.12 +verified_revision: 61e473c627866e06f7474e843d2a077ae67f76e2 +verified_at: 2026-09-04T19:59:00Z evidence: - uri: ci://wave-av/sdks/.github/workflows/registry-cleanroom.yml#cleanroom-report.json - sha256: 0f03603df2bc33f50958a75c8998777463507c1f9e3ee0fec811d82a3ca1206d + sha256: ddb12dfc51476c1d4406806fac37f096474c68a698e41fce1a5815767ba8f10d pass_condition_from_spec: > Every shipped component resolves to one source revision and version; no newer source is represented as deployed; mutable channels are labeled; deployment receipt identifies artifact digest. notes: | This repository covers the REGISTRY half of VER-001 — does the artifact agree with itself about - which build it is. Two published artifacts do not: - @wave-av/cli@1.0.8 binary self-reports 1.0.0 - @wave-av/mcp-server@0.2.0 serverInfo.version reports 0.1.0 - In both cases the package metadata is correct and the code inside it disagrees, so a version - comparison against the registry cannot see the defect — only running the artifact can. + which build it is. Re-verified 2026-09-04: + @wave-av/cli@1.0.8 binary self-reports 1.0.0. FIXED in `wave-av/cli` source + (commit 91093d5, `src/lib/version.ts` now derives the version from + `package.json` at runtime); pending publish. + @wave-av/mcp-server@0.2.1 serverInfo.version now correctly reports 0.2.1 — self-resolved live. + Source on `origin/main` still hardcoded the literal that caused the + original defect; hardened in THIS commit (new `src/version.ts`, + wired into `server.ts`, regression test added) so the next publish + cannot regress it. + In both cases the package metadata was correct and the code inside it disagreed, so a version + comparison against the registry alone cannot see the defect — only running the artifact can. Tag/GitHub-release/deployed-endpoint agreement is NOT covered here and remains unknown; it belongs to the release-ledger check named in the spec's runnable_command. ``` @@ -124,12 +133,12 @@ notes: | criterion_id: SUPPLY-001 status: fail owner: WAVE platform / SDK publishing -verification_command: node scripts/ga/registry-cleanroom.mjs -verified_revision: 70b2a04a205658a25b3723adec9faf665e204af6 -verified_at: 2026-09-04T00:31:56Z +verification_command: node scripts/ga/registry-cleanroom.mjs --python python3.12 +verified_revision: 61e473c627866e06f7474e843d2a077ae67f76e2 +verified_at: 2026-09-04T19:59:00Z evidence: - uri: ci://wave-av/sdks/.github/workflows/registry-cleanroom.yml#cleanroom-report.json - sha256: 0f03603df2bc33f50958a75c8998777463507c1f9e3ee0fec811d82a3ca1206d + sha256: ddb12dfc51476c1d4406806fac37f096474c68a698e41fce1a5815767ba8f10d pass_condition_from_spec: > Release artifacts are built by approved CI from an immutable source revision, provenance is verifiable, SBOM is attached, critical known vulnerabilities are resolved or explicitly @@ -137,15 +146,15 @@ pass_condition_from_spec: > notes: | PARTIAL COVERAGE — this evaluator checks two of the five clauses. A `fail` here is therefore sound, but a future `pass` would NOT be sufficient to pass SUPPLY-001 on its own. - Covered and failing: + Covered and failing (both already fixed in `wave-av/cli` source, pending publish — verified + 2026-09-04 against `wave-av/cli`'s `origin/main`): provenance @wave-av/cli@1.0.8 carries no npm provenance attestation (dist.attestations is null), while sdk, mcp-server and adk each carry a - https://slsa.dev/provenance/v1 attestation. The CLI was published outside the - provenance-emitting pipeline and cannot be traced to an approved CI build. - dependency @wave-av/cli@1.0.8 declares `@wave-av/sdk: "^2.0.11"`. What a customer receives - policy is decided by npm's resolver on the day they install; today that is 2.1.3. The - published artifact is not reproducible, and this is the precise mechanism by - which a broken SDK shipped inside a CLI that no one had changed. + https://slsa.dev/provenance/v1 attestation. `.github/workflows/release.yml` + already runs `npm publish --provenance` under OIDC trusted publishing — + `1.0.8` predates that pipeline; the next publish carries provenance. + dependency @wave-av/cli@1.0.8 declares `@wave-av/sdk: "^2.0.11"`. `package.json` on + policy `origin/main` already pins it exact (`2.0.14`); pending publish. NOT covered here, still unknown: SBOM attachment, vulnerability posture, publisher MFA and branch-protection attestation. ``` @@ -163,40 +172,41 @@ It is **not yet a required branch-protection status check**, deliberately. `clea tests artifacts published from **two other repositories** — `wave-av/cli` (`@wave-av/cli`) and `wave-av/sdk-python` (`wave-sdk` on PyPI) — that an `sdks` PR cannot fix by itself. Requiring it today would red every future `sdks` PR for a defect it did not introduce, training reviewers to -override rather than read it. Sequence: - -1. This repo's own defect (`wave-av-sdk`'s `wave`/stdlib collision, ART-001) is fixed **in - source** in this same change (`wave/` renamed to `wave_sdk/`, version bumped to `3.0.0`) but - remains **unpublished** — publishing to PyPI is an operator-gated action (Trusted Publisher - registration + the `pypi-publish` environment's required reviewer), not something this lane - may cross. `cleanroom` will keep reporting `wave-av-sdk py-import-module` / - `py-no-stdlib-shadow` as FAIL against the live registry until that publish happens — correctly, - because it tests what PyPI serves, never this checkout. -2. `wave-av/cli` fixes and republishes with: `wave --version` reading from its own - `package.json` (not a stale constant), `@wave-av/sdk` pinned exact rather than `^2.0.11`, and - npm provenance attestation present. -3. `wave-av/sdk-python` fixes the same stdlib-collision defect in `wave-sdk` (that package is a - distinct source tree from this repo's `wave-av-sdk` — see `CHANGELOG.md`'s note — so this - commit does not touch it). -4. Once `node scripts/ga/registry-cleanroom.mjs` reports **zero** failing checks against the - live registries, add `registry clean-room acceptance / cleanroom` to the default branch's - required status checks. It already runs on every PR with no path filter, so it can be made - required without a permanently-unreported gap. - -`@wave-av/mcp-server`'s `serverInfo.version` mismatch (0.1.0 vs 0.2.0), one of the original 8 -failures, is **already resolved**: the live registry now serves `@wave-av/mcp-server@0.2.1` with -`serverInfo.version` `0.2.1` (re-verified 2026-09-04 against `registry.npmjs.org` and the -package's own stdio `initialize` response — see the clean-room run below). 7 checks remain -failing, all pre-existing and none introduced by this change: 3 in `wave-av/cli`, 2 in -`wave-av/sdk-python`, 2 (`wave-av-sdk` import + stdlib-shadow) fixed in source here and pending -publish. +override rather than read it. + +**Re-verified 2026-09-04 (this change): all 8 originally-failing checks are now root-cause fixed +in source, across three repositories. None remains an open defect — every one is either already +live or blocked ONLY on an operator-gated publish, which this lane may not cross.** + +| # | Check | Source fix | Where | Status | +|---|---|---|---|---| +| 1 | `@wave-av/cli` `npm-provenance-attested` | `release.yml` already runs OIDC trusted publishing with `npm publish --provenance` | `wave-av/cli` (verified: `.github/workflows/release.yml`) | fixed in source; `1.0.8` predates it — next publish carries provenance | +| 2 | `@wave-av/cli` `bin-version-matches-package` | `src/lib/version.ts` derives `CLI_VERSION` from `package.json` at runtime instead of a hardcoded literal | `wave-av/cli` commit `91093d5` (2026-09-03, "fix(version): derive every CLI version surface from package.json (VER-001)") | fixed in source; pending publish (`package.json` already at `1.0.9`) | +| 3 | `@wave-av/cli` `declared-dep-ranges-pinned` | `@wave-av/sdk` dependency changed from `^2.0.11` to an exact `2.0.14` | `wave-av/cli` `package.json` (verified on `origin/main`) | fixed in source; pending publish | +| 4 | `@wave-av/mcp-server` `mcp-serverinfo-version-matches-package` | live artifact `0.2.1` already reports the correct `serverInfo.version` — BUT `src/server.ts` on `origin/main` still hardcoded `version: "0.1.0"` in the `McpServer` constructor, a latent regression: the next publish built from unmodified `main` would have reintroduced the exact defect. | **this repo, this commit** — new `sdk-typescript/packages/mcp-server/src/version.ts` (mirrors the CLI's pattern: walk up from the module's own location to find `package.json`, verify its `name` matches, read `version`), wired into `server.ts`; regression test `__tests__/version.test.ts` added; `vitest`/`test` script added to `package.json` (was silently absent — no other package in the workspace lacks it) | **source hardened and verified** (`tsc --noEmit` clean, `vitest run __tests__/version.test.ts` 2/2 pass, and the built `dist/index.js` was probed live over stdio JSON-RPC: `serverInfo.version` now reads `0.1.8`, this repo's current `package.json` version, not a literal) | +| 5 | `wave-sdk` (PyPI) `py-import-module` | top-level module renamed `wave` → `wave_sdk` | `wave-av/sdk-python` (**separate repo** — this PyPI distribution is built from `github.com/wave-av/sdk-python`, not this monorepo's `sdk-python/`; confirmed via the live package's own `project_urls.Repository`) — already on `origin/main` at version `2.1.0`, package dir is `wave_sdk/` | fixed in source in that repo already; pending publish; **out of this repo's scope to publish or PR** | +| 6 | `wave-sdk` (PyPI) `py-no-stdlib-shadow` | same rename as #5 | `wave-av/sdk-python` | same as #5 | +| 7 | `wave-av-sdk` (PyPI) `py-import-module` | top-level module renamed `wave` → `wave_sdk`, `pyproject.toml` `include` updated, all internal imports + tests + docs updated, version bumped `2.0.1` → `3.0.0` | **this repo, this commit** — `sdk-python/` | fixed in source; verified with a real build (`python -m build` + fresh venv install + the actual `cleanroom_python_assert.py` probe: `py-import-module` OK, `py-no-stdlib-shadow` OK); `pytest` 31/31 pass; pending publish | +| 8 | `wave-av-sdk` (PyPI) `py-no-stdlib-shadow` | same rename as #7 | same as #7 | same as #7 | + +Sequence to close the arming window and make `cleanroom` a required check: + +1. Publish `wave-av/cli` (carries fixes #1–#3). +2. Publish `wave-av/sdk-python` under the `wave-sdk` PyPI project (carries fixes #5–#6) — a + different repo and a different operator action than this one. +3. Publish `wave-av-sdk` from a `sdk-python-v3.0.0` tag on **this** repo (carries fixes #7–#8). +4. Once `node scripts/ga/registry-cleanroom.mjs` reports zero failing checks against the live + registries, add `registry clean-room acceptance / cleanroom` to the default branch's required + status checks. It already runs on every PR with no path filter, so it can be made required + without a permanently-unreported gap. + +None of steps 1–3 is a source change this lane is blocked on producing — they are blocked on an +operator triggering a publish (npm/PyPI Trusted Publisher, `pypi-publish` environment +required-reviewer), which is explicitly outside what an automated lane may do. ## Operator actions to finish these criteria -1. **Make the release gate blocking** once the arming window above closes (all 7 checks pass). -2. **Fix the remaining artifact defects the gate found** (each needs a publish, which is a named - floor and not this lane's to cross): the CLI version constant and dependency pin live in - `wave-av/cli`; the Python distribution's top-level module name for `wave-sdk` lives in - `wave-av/sdk-python`. `wave-av-sdk`'s equivalent is fixed in source in this commit. -3. **Republish the CLI through the provenance-emitting workflow** so `dist.attestations` is - populated, and exact-pin its first-party dependency. +1. **Publish** `wave-av/cli`, `wave-av/sdk-python`, and this repo's `sdk-python-v3.0.0` tag — the + only remaining action; every source defect the gate found is already fixed (table above). +2. **Make the release gate blocking** once all three publishes land and `cleanroom` reports zero + failures against the live registries. diff --git a/sdk-typescript/packages/mcp-server/__tests__/version.test.ts b/sdk-typescript/packages/mcp-server/__tests__/version.test.ts new file mode 100644 index 0000000..20a418a --- /dev/null +++ b/sdk-typescript/packages/mcp-server/__tests__/version.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { MCP_SERVER_VERSION, UNKNOWN_VERSION } from '../src/version.js'; + +const PKG_PATH = join(__dirname, '../package.json'); +const PKG_VERSION = (JSON.parse(readFileSync(PKG_PATH, 'utf-8')) as { version: string }).version; +const SERVER_FILE = join(__dirname, '../src/server.ts'); + +/** + * VER-001 — @wave-av/mcp-server@0.2.0 reported `serverInfo.version="0.1.0"` because + * `server.ts` passed a hardcoded string literal to `new McpServer({ version: "0.1.0" })` + * instead of deriving it from package.json. An MCP client cannot identify the build it + * connected to when the two disagree (registry clean-room acceptance, GA-2026-09-04). + * + * These tests fail if MCP_SERVER_VERSION drifts from package.json, or if a new hardcoded + * semver-shaped literal is reintroduced into server.ts's `new McpServer({...})` call. + */ +describe('VER-001: MCP server version is a single source of truth', () => { + it('derives MCP_SERVER_VERSION from package.json', () => { + expect(MCP_SERVER_VERSION).toBe(PKG_VERSION); + expect(MCP_SERVER_VERSION).not.toBe(UNKNOWN_VERSION); + }); + + it('never hardcodes a version literal in the McpServer constructor', () => { + const src = readFileSync(SERVER_FILE, 'utf-8'); + // The bug shipped as literally `version: "0.1.0"`. Match ANY quoted semver-shaped + // literal passed as `version:` so a different hardcoded value doesn't slip back in. + const literal = /version:\s*["'`]\d+\.\d+\.\d+["'`]/.exec(src); + expect(literal, `found a hardcoded version literal in server.ts: ${literal?.[0]}`).toBeNull(); + expect(src).toContain('MCP_SERVER_VERSION'); + }); +}); diff --git a/sdk-typescript/packages/mcp-server/package.json b/sdk-typescript/packages/mcp-server/package.json index 915474d..a99d37a 100644 --- a/sdk-typescript/packages/mcp-server/package.json +++ b/sdk-typescript/packages/mcp-server/package.json @@ -26,6 +26,7 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "test": "vitest run", "type-check": "tsc --noEmit", "lint": "eslint src/", "prepublishOnly": "npm run build" @@ -69,6 +70,7 @@ "devDependencies": { "@types/node": "^22.13.0", "tsup": "^8.0.0", - "typescript": "^5.9.0" + "typescript": "^5.9.0", + "vitest": "^4.1.4" } } diff --git a/sdk-typescript/packages/mcp-server/src/server.ts b/sdk-typescript/packages/mcp-server/src/server.ts index 71c7379..def3864 100644 --- a/sdk-typescript/packages/mcp-server/src/server.ts +++ b/sdk-typescript/packages/mcp-server/src/server.ts @@ -12,6 +12,7 @@ import { registerAgenticMediaTools } from "./tools/agentic-media.js"; import { registerGatewayTools } from "./tools/gateway.js"; import { registerStreamResources } from "./resources/streams.js"; import { registerProductionResources } from "./resources/productions.js"; +import { MCP_SERVER_VERSION } from "./version.js"; export async function startServer(): Promise { // P18.4 #4 — Initialize Dash0 OTLP visibility (no-op when DASH0_AUTH_TOKEN unset). @@ -27,7 +28,7 @@ export async function startServer(): Promise { const server = new McpServer({ name: "wave-mcp-server", - version: "0.1.0", + version: MCP_SERVER_VERSION, }); // Register tools diff --git a/sdk-typescript/packages/mcp-server/src/version.ts b/sdk-typescript/packages/mcp-server/src/version.ts new file mode 100644 index 0000000..e8ea99b --- /dev/null +++ b/sdk-typescript/packages/mcp-server/src/version.ts @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, parse } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * The single source of truth for "what version of the WAVE MCP server is this?". + * + * `serverInfo.version` reported over the MCP protocol MUST derive from this module. A + * hardcoded version literal in `server.ts` is the defect class, not a typo: the published + * server shipped `version: "0.1.0"` as a literal that stopped tracking package.json, so + * clients connecting to @wave-av/mcp-server@0.2.0 were told they were talking to 0.1.0 — + * an MCP client cannot identify the build it connected to (VER-001, GA-2026-09-04). + * + * Resolution walks UP from this module's own location to the nearest directory holding a + * package.json with a string `version`. That is deliberately depth-independent: in + * development this file is `src/version.ts` (one level below the package root), while the + * shipped bundle is a single `dist/index.js` (also one level below it) — but a fixed + * `../package.json` would be one specific hop that silently breaks the moment either layout + * changes depth, which is how depth-coupled version reads break at publish time. + */ + +/** Returned when package.json cannot be located or parsed — never a plausible-looking version. */ +export const UNKNOWN_VERSION = "0.0.0-unknown"; + +function readOwnVersion(): string { + try { + let dir = dirname(fileURLToPath(import.meta.url)); + const { root } = parse(dir); + + for (;;) { + const candidate = join(dir, "package.json"); + if (existsSync(candidate)) { + const pkg = JSON.parse(readFileSync(candidate, "utf-8")) as { name?: unknown; version?: unknown }; + // Only accept a package.json that actually IS this package — walking up past the + // package root (e.g. into a monorepo's own package.json, or a consuming app's) would + // silently report the WRONG package's version. + if (pkg.name === "@wave-av/mcp-server" && typeof pkg.version === "string" && pkg.version.length > 0) { + return pkg.version; + } + } + + if (dir === root) break; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + + return UNKNOWN_VERSION; + } catch { + return UNKNOWN_VERSION; + } +} + +/** The running server's version, read once from package.json at process start. */ +export const MCP_SERVER_VERSION = readOwnVersion(); diff --git a/sdk-typescript/pnpm-lock.yaml b/sdk-typescript/pnpm-lock.yaml index 3426b2c..0942963 100644 --- a/sdk-typescript/pnpm-lock.yaml +++ b/sdk-typescript/pnpm-lock.yaml @@ -563,6 +563,9 @@ importers: typescript: specifier: ^5.9.0 version: 5.9.3 + vitest: + specifier: ^4.1.4 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@22.19.19)(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)) packages/mesh: dependencies: @@ -2799,6 +2802,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7))': + dependencies: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.14(@types/node@22.19.19)(esbuild@0.27.7) + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7))': dependencies: '@vitest/spy': 4.1.7 @@ -3513,6 +3524,18 @@ snapshots: vary@1.1.2: {} + vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.0.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.19.19 + esbuild: 0.27.7 + fsevents: 2.3.3 + vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7): dependencies: lightningcss: 1.33.0 @@ -3525,6 +3548,34 @@ snapshots: esbuild: 0.27.7 fsevents: 2.3.3 + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@22.19.19)(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@22.19.19)(esbuild@0.27.7)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.3 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.14(@types/node@22.19.19)(esbuild@0.27.7) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 22.19.19 + transitivePeerDependencies: + - msw + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.27.7)): dependencies: '@vitest/expect': 4.1.7