Skip to content

Commit 92f3c11

Browse files
GitHub #686: Future work: preserve protocol neutrality for possible SDKs beyond PHP and Python (#507)
1 parent 1f3e7c7 commit 92f3c11

5 files changed

Lines changed: 787 additions & 3 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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.

src/V2/Support/PlatformProtocolSpecs.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ final class PlatformProtocolSpecs
3838
{
3939
public const SCHEMA = 'durable-workflow.v2.platform-protocol-specs.catalog';
4040

41-
public const VERSION = 10;
41+
public const VERSION = 11;
4242

4343
public const AUTHORITY_URL = 'https://durable-workflow.github.io/docs/2.0/platform-protocol-specs';
4444

@@ -650,7 +650,7 @@ private static function specs(): array
650650
'spec_path' => 'static/platform-protocol-specs/mcp-tool-results.schema.json',
651651
],
652652
'cluster_info_envelope' => [
653-
'description' => 'JSON Schema for the `GET /api/cluster/info` envelope: identity, capability, topology, coordination-health, and the nested protocol manifests (`client_compatibility`, `control_plane`, `worker_protocol`, `surface_stability_contract`, `platform_protocol_specs`, `platform_conformance_suite`, `auth_composition_contract`, `bridge_adapter_outcome_contract`, `replay_verification_contract`). The envelope is the discovery surface that every other catalog entry can be reached from.',
653+
'description' => 'JSON Schema for the `GET /api/cluster/info` envelope: identity, capability, topology, coordination-health, and the nested protocol manifests (`client_compatibility`, `control_plane`, `worker_protocol`, `surface_stability_contract`, `platform_protocol_specs`, `platform_conformance_suite`, `sdk_neutrality_contract`, `auth_composition_contract`, `bridge_adapter_outcome_contract`, `replay_verification_contract`). The envelope is the discovery surface that every other catalog entry can be reached from.',
654654
'format' => self::FORMAT_JSON_SCHEMA,
655655
'spec_id' => 'durable-workflow.v2.cluster-info-envelope',
656656
'surface_family' => 'cluster_info_manifests',
@@ -688,6 +688,12 @@ private static function specs(): array
688688
'schema_authority' => 'Workflow\\V2\\Support\\PlatformConformanceSuite::SCHEMA',
689689
'version_authority' => 'Workflow\\V2\\Support\\PlatformConformanceSuite::VERSION',
690690
],
691+
[
692+
'name' => 'sdk_neutrality_contract',
693+
'owner_repo' => 'durable-workflow/workflow',
694+
'schema_authority' => 'Workflow\\V2\\Support\\SdkNeutralityContract::SCHEMA',
695+
'version_authority' => 'Workflow\\V2\\Support\\SdkNeutralityContract::VERSION',
696+
],
691697
],
692698
'evolution_rule' => self::EVOLUTION_ADDITIVE_MINOR_BREAKING_MAJOR,
693699
'breaking_change_release' => 'major',

0 commit comments

Comments
 (0)