|
| 1 | +# SDK Neutrality Contract |
| 2 | + |
| 3 | +This document is the human-readable authority for the SDK neutrality |
| 4 | +contract enforced by `Workflow\V2\Support\SdkNeutralityContract`. It |
| 5 | +sits downstream of the platform compatibility authority |
| 6 | +(`SurfaceStabilityContract`) and the platform protocol-spec catalog |
| 7 | +(`PlatformProtocolSpecs`). Where this document and one of those |
| 8 | +authorities disagree, the upstream authority wins and this document |
| 9 | +is the bug. |
| 10 | + |
| 11 | +## Why this exists |
| 12 | + |
| 13 | +Durable ships two first-party SDKs: the PHP `durable-workflow/workflow` |
| 14 | +package and the Python `durable_workflow` package. There is no plan to |
| 15 | +ship a broad official SDK portfolio in the v2 release, and there is no |
| 16 | +reserved release slot for a TypeScript, Go, Java, or .NET SDK. |
| 17 | + |
| 18 | +That choice is intentional. The maintenance cost of a wide first-party |
| 19 | +SDK roster is high and the demand for SDKs in those ecosystems has not |
| 20 | +yet been demonstrated. |
| 21 | + |
| 22 | +What we do not want, however, is for the public contracts under those |
| 23 | +SDKs to quietly hard-code PHP-only or Python-only assumptions. If a |
| 24 | +future TypeScript or Go SDK becomes worth building, the work should be |
| 25 | +"write a new client against the published wire protocol", not |
| 26 | +"redesign the protocol so a non-PHP, non-Python language can speak it |
| 27 | +at all". |
| 28 | + |
| 29 | +This contract is the standing rule that protects that property. |
| 30 | + |
| 31 | +## Scope |
| 32 | + |
| 33 | +- **Goal**: every public Durable contract is shaped so a future |
| 34 | + TypeScript, Go, Java, or .NET SDK could be written without breaking |
| 35 | + the wire protocol or the replay-fixture corpus. |
| 36 | +- **Non-goal**: shipping additional first-party SDKs. Breadth is |
| 37 | + demand-driven. New first-party SDKs are added only when adoption |
| 38 | + signal justifies the maintenance commitment. |
| 39 | +- **Present priority**: Python is the highest-value non-PHP SDK. It |
| 40 | + exists today and is treated as a parity-coverage priority surface. |
| 41 | +- **Future posture**: TypeScript, Go, Java, .NET, and other languages |
| 42 | + are demand-driven. They have no reserved release slot, but every |
| 43 | + public contract must be implementable in any of them using only the |
| 44 | + language's standard HTTP and JSON tooling and the published spec |
| 45 | + catalog. |
| 46 | + |
| 47 | +The `SdkNeutralityContract` class enumerates these as `posture` values |
| 48 | +on each language entry and on the `expansion_criteria` map. |
| 49 | + |
| 50 | +## Neutrality rules |
| 51 | + |
| 52 | +The contract defines seven minimum neutrality rules. Every public |
| 53 | +contract must satisfy each of them. The class is the authority for the |
| 54 | +exact field shapes; the summary below is for reviewers. |
| 55 | + |
| 56 | +| Rule | Requirement | |
| 57 | +| --- | --- | |
| 58 | +| `protocol_neutrality` | Public RPC and event surfaces use HTTP+JSON or AsyncAPI shapes that any HTTP-capable runtime can produce and consume. | |
| 59 | +| `codec_neutrality` | Every payload that crosses a public boundary advertises a codec name. At least one universal codec is always offered alongside any engine-specific codec. | |
| 60 | +| `error_shape_neutrality` | Public failure objects use a structured envelope of (`code`, `message`, optional `details`). PHP/Python exception class names are diagnostic only. | |
| 61 | +| `type_identity_neutrality` | Workflow, activity, child workflow, and exception types are identified by stable string names. Class FQCNs are SDK-input convenience, not contract. | |
| 62 | +| `replay_fixture_neutrality` | Replay fixtures and golden history bundles are JSON conforming to the published `history_event_payloads` and `replay_bundle` schemas. | |
| 63 | +| `discovery_neutrality` | Every public surface is reachable from `GET /api/cluster/info` and the `platform_protocol_specs` catalog. | |
| 64 | +| `documentation_neutrality` | Public-contract docs describe shapes in schema, route, and field semantics. PHP and Python class behaviour appears as SDK examples, not as the normative contract. | |
| 65 | + |
| 66 | +The full rationale, authority pointer, and "how to apply" for each rule |
| 67 | +is on the matching `neutrality_rules` entry of the class manifest. |
| 68 | + |
| 69 | +## Standing audit checklist |
| 70 | + |
| 71 | +Every new server, workflow, CLI, Waterline, or MCP surface must clear |
| 72 | +the `audit_checklist` before promotion to `stable`. The checklist is a |
| 73 | +standing review item on every release PR that touches an audit-scoped |
| 74 | +surface family. The audit-scoped families today are: |
| 75 | + |
| 76 | +- `server_api` |
| 77 | +- `worker_protocol` |
| 78 | +- `cli_json` |
| 79 | +- `waterline_api` |
| 80 | +- `mcp_discovery_results` |
| 81 | +- `cluster_info_manifests` |
| 82 | + |
| 83 | +The checklist has eight steps. Seven correspond to the neutrality rules |
| 84 | +above. The eighth is a thought experiment: the reviewer must be able to |
| 85 | +describe in two sentences how a TypeScript or Go SDK would consume the |
| 86 | +new surface using only the published spec catalog and a standard |
| 87 | +HTTP+JSON toolchain. If the answer requires a first-party SDK, the |
| 88 | +surface is not neutral and either the surface is reshaped or the |
| 89 | +neutrality gap is recorded as a known limitation before promotion. |
| 90 | + |
| 91 | +## SDK breadth policy |
| 92 | + |
| 93 | +The `sdk_breadth_policy` map on the manifest is the source of truth for |
| 94 | +the official-SDK roster: |
| 95 | + |
| 96 | +- `first_party.php_workflow_package`: posture `priority`. Reference |
| 97 | + workflow authoring SDK and embedded host. |
| 98 | +- `first_party.python_sdk`: posture `priority`. Highest-value non-PHP |
| 99 | + SDK; used to validate that the worker protocol, control plane, and |
| 100 | + replay fixtures behave the same way outside PHP. |
| 101 | +- `demand_driven.typescript_sdk`, `go_sdk`, `java_sdk`, `dotnet_sdk`: |
| 102 | + posture `demand_driven`. No first-party SDK exists. Public contracts |
| 103 | + must remain implementable in those languages without protocol |
| 104 | + redesign. |
| 105 | + |
| 106 | +A new first-party SDK is added only when: |
| 107 | + |
| 108 | +1. There is documented user demand the existing SDKs cannot serve. |
| 109 | +2. A candidate maintainer team commits to keeping the SDK on the |
| 110 | + conformance harness, the protocol-spec catalog, and the release |
| 111 | + authority manifest. |
| 112 | +3. Adding the SDK does not require breaking changes to the worker |
| 113 | + protocol, control plane, history-event wire formats, or replay |
| 114 | + fixtures. If it would, the protocol is the bug, not the SDK. |
| 115 | + |
| 116 | +## What a future SDK relies on |
| 117 | + |
| 118 | +The contract identifies the surfaces a future SDK must be able to read |
| 119 | +without inspecting any first-party SDK source. These are the |
| 120 | +load-bearing inputs for any non-PHP, non-Python SDK: |
| 121 | + |
| 122 | +- **Protocol**: the `control_plane_api`, `worker_protocol_api`, and |
| 123 | + `worker_protocol_stream` spec entries in the |
| 124 | + `PlatformProtocolSpecs` catalog. |
| 125 | +- **Codecs**: the universal codec set advertised by |
| 126 | + `Workflow\Serializers\CodecRegistry::universal()` and surfaced on |
| 127 | + the `worker_protocol` cluster_info manifest. |
| 128 | +- **Error shape**: the `external_task_result_contract` failure |
| 129 | + envelope and the `repair_actionability_objects` schemas. |
| 130 | +- **Replay fixtures**: the `history_event_payloads` and |
| 131 | + `replay_bundle` JSON Schemas plus the `history_replay_bundles` |
| 132 | + fixture category in the `PlatformConformanceSuite`. |
| 133 | +- **Discovery**: the `cluster_info_envelope` schema and the |
| 134 | + `platform_protocol_specs` catalog itself. |
| 135 | + |
| 136 | +If any of those surfaces is not reachable for a candidate SDK in a |
| 137 | +given language, building the SDK requires protocol changes and the |
| 138 | +language-agnosticism guarantee is not being honored. |
| 139 | + |
| 140 | +## Release gates |
| 141 | + |
| 142 | +A release that introduces a new public surface family or promotes an |
| 143 | +existing surface from `prerelease` or `experimental` to `stable` must |
| 144 | +record the audit outcome on the release PR. The `release_gates.gates` |
| 145 | +map enumerates the specific checks. Enforcement is a mix of: |
| 146 | + |
| 147 | +- **Machine**: tests under `tests/Unit/V2/SdkNeutralityContractTest.php` |
| 148 | + pin the manifest, the docs site CI cross-references the audit scope |
| 149 | + against the surface stability families, and the conformance harness |
| 150 | + rejects fixtures that do not validate against the published JSON |
| 151 | + Schemas. |
| 152 | +- **Human**: release reviewers tick the SDK-neutrality audit on every |
| 153 | + release PR that adds or promotes a public surface. The reviewer is |
| 154 | + responsible for the future-SDK thought experiment. |
| 155 | + |
| 156 | +## Changing this contract |
| 157 | + |
| 158 | +Adding a neutrality rule, tightening an existing rule, adding a |
| 159 | +required audit step, adding a surface family to the audit scope, or |
| 160 | +changing the official-SDK breadth policy is a contract change. Bump |
| 161 | +`SdkNeutralityContract::VERSION`, update this document, the static |
| 162 | +JSON mirror (when one is published), and the per-package stability |
| 163 | +documents in the same change. Removing a neutrality rule or audit |
| 164 | +step is a major change. |
0 commit comments