ADR 0028 — Revert createContractDispatcher (no adopting consumer)
- Status: Accepted — supersedes the
createContractDispatcherportion of ADR 0027. The rest of ADR 0027 (idempotent,createRetainedTopics, the openTransportSourceunion) stays Accepted. - Date: 2026-06-17
Context
ADR 0027 shipped
createContractDispatcher in 0.9.0 — a contract-shaped wrapper over the internal
executeToolMethod, so a bring-your-own transport (a raw-WebSocket lane, an IPC
channel, a queue worker) could run a defineContract method without a hand-rolled
method registry. It was built for one requesting consumer (a desktop app’s
webview ↔ local-sidecar raw-WebSocket lane).
On real integration that consumer did not adopt it, and reported why:
- Their boundary already had a ~39-line executor (parse-in → run → parse-out) with distributed method registration — the registry the dispatcher removes was tiny.
- The one benefit they wanted — the typed
{ ok, code, … }envelope — they got directly by threadingcodethrough their own WebSocket envelope (~10 lines). - Migrating to the dispatcher meant rewriting their flat runtime schema map into
a
defineContract(each endpoint needsmethod/path/desc), a net +90 to +110 lines — more code, for “one mental model” they did not need.
So for the only consumer it was built for — and exactly the case it was designed for (raw-WS webview ↔ sidecar) — the dispatcher was net-negative. No other consumer uses it. Its evidence base is now zero.
Decision
Remove createContractDispatcher (and the ContractDispatcher /
ContractDispatcherConfig types) from stitchkit/tools. The execution core
(executeToolMethod) stays internal, exactly where it was before 0.9.0 — the MCP
and agent mounts still use it; only the public BYO-transport wrapper is withdrawn.
This upholds the project’s own bar (ROADMAP / ADR 0027
itself): public surface comes from real usage, not speculation. Carrying an
export that the sole requesting consumer rejected — “just in case” — is the
speculation the pre-1.0 process exists to avoid. The capability is trivially
re-exposable from executeToolMethod if a genuine second consumer (an IPC channel
or queue worker — not the webview↔sidecar case, which was tried) proves the need
on evidence.
Kept from 0.9.0 / ADR 0027 — these are in use or harmless and unchanged:
idempotenton an endpoint — a transport-neutral hint, in use.createRetainedTopics— sticky events, in production use.MultipartFile/FileDescriptor— React Native upload, in production use.TransportSourceopen union (| (string & {})) — a one-line type widening with no runtime; a consumer running its own transport viarawRoutesstill legitimately tagsctx.source. Reverting it would be churn with no benefit.
Alternatives considered
- Keep it “just in case.” Rejected — it is known-unused public API on the road to 1.0, where “every export is proven” is the goal. Keeping a dead export is worse for 1.0 credibility than a documented, one-time breaking removal; pre-1.0 breaking changes are allowed when not silent, and the caret range protects every consumer except the one who never used it.
- Also revert
TransportSourceopen /idempotent. Rejected — both are harmless, additive, and independently reasonable for any BYO transport (not only the dispatcher). Only the dispatcher had no adopter.
Consequences
- Breaking change (removed public export) — led in the CHANGELOG with a
### ⚠️ Breaking changessection, shipped under a minor bump (0.10.0). A consumer that adoptedcreateContractDispatcherruns the contract method itself: validate the frame against the contract’s Zod schemas and call the handler (the ~40-line executor the requesting consumer already had). - Leaner core toward 1.0. No unused transport surface to keep stable, tested and documented.
- Reversible on evidence.
executeToolMethodis unchanged; a future ADR can re-expose a BYO executor when a real, different consumer needs it.