Files
paseo/docs/protocol-compatibility.md
Mohamed Boudra 588fdc794b docs: rewrite contributing guide, add QA and compatibility docs
Issues are bug-only now. The feature-request template is gone, requests
opened as issues get closed automatically, and feature talk moves to
product discussions on GitHub Discussions and Discord. "Product
discussion" is the term everywhere, recorded in the glossary.

Contributing states the QA evidence bar and links out instead of
teaching; docs/qa.md carries the depth. The protocol back-compat rules
moved out of CLAUDE.md into docs/protocol-compatibility.md, since that
constraint is invisible in development where both sides always run the
same version.

CLAUDE.md gains doc-writing and voice rules to keep docs from
accumulating appended paragraphs. design.md gains an alignment section
(glyph rails) and a layout-shift rule.
2026-07-30 15:52:50 +02:00

60 lines
3.6 KiB
Markdown

# Protocol Compatibility
The app and the daemon are separate products that ship separately. A user updates the app from an app store or a desktop auto-update; they update the daemon when they feel like it. Every combination happens in the wild: new app against an old daemon, old app against a new daemon, and both sides months apart.
In development both sides are always the same version, which is why this is the constraint contributors miss most often.
Two contracts follow from it.
## The protocol contract: always compatible
A schema change must not break parsing in either direction. An old app still parses messages from a new daemon. A new daemon still parses messages from an old app.
- New fields are `.optional()` with a sensible default.
- Never flip optional to required, remove a field, or narrow a type. `string` to `enum` and nullable to non-null are both narrowing.
- A field you stop sending stays accepted. You stop writing it, you don't stop reading it.
- Wire schemas are pure structural declarations. No `.transform()`, `.catch()`, or `.preprocess()` on WebSocket message schemas — normalization happens in an explicit pass after validation. The reason is in [protocol-validation.md](protocol-validation.md): inbound validators are generated, and the generator only compiles pure schemas.
- Plain `z.union()` is forbidden when every branch shares a literal tag. Use `z.discriminatedUnion()`.
- `.default()` belongs on primitive leaves only, never on item schemas inside large arrays or big inbound containers.
Two questions to ask before you commit a schema change:
1. Does a six-month-old app still parse this message?
2. Does a six-month-old daemon still send something this app accepts?
If you can't answer both with yes, the change isn't done.
Schemas live in `packages/protocol/src/messages.ts`. New RPC names follow [rpc-namespacing.md](rpc-namespacing.md).
## The feature contract: per feature, gated once
Features don't have to work across versions. A new feature usually needs a new daemon capability, and old daemons don't have it.
The app checks for the capability and either runs the feature or tells the user to update the host.
- **No fallback paths.** Don't build a degraded version of the feature for old daemons. Don't fan out across legacy RPCs to simulate a capability that isn't there. The user updates or doesn't get the feature.
- **No defensive branches spread through the feature.** Detection happens in one place, and everything downstream reads a clean shape.
- Capability flags live in `features` on the `server_info` message (`packages/protocol/src/messages.ts`, the `server_info` schema).
Existing functionality keeps working across versions because of the protocol contract. Gating a new feature never substitutes for that.
## Every shim is tagged and dated
A shim that exists for old-app or old-daemon support carries a comment naming it, the version it arrived in, and when it can go:
```ts
// COMPAT(workspaceFileEditing): added in v0.2.0, remove after 2027-01-18 once daemon floor >= v0.2.0.
```
`rg "COMPAT\("` is the full cleanup backlog, so:
- One tag per shim, at the site that has to be deleted.
- Give it a name, a version, and a removal condition or date. Six months out is the usual default.
- Never bury compatibility in an untagged `??` fallback or an optional-chain tunnel. Untagged back-compat never gets removed, because nobody can find it.
When a tag's condition is met, delete the shim and the tag in the same change.
## QA
Tests don't fully cover compatibility. If you touched `packages/protocol`, say in the pull request why an older app still parses your message and why an older daemon still satisfies your app. See [qa.md](qa.md).