You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(protocol): put the realtime subscribe fences on the shape SubscribeMessageSchema declares (#17747)
Part of #17184 — the **second half**. The first half (the four
undeclared message types) landed as #17700; this PR does not revisit it.
- **Clause-②: no** — this PR puts no new key on any published payload.
`packages/spec/src/api/websocket.zod.ts` is **byte-unchanged**: blob
`3348dd76745b61e83801e2e8bae48c6756c5a69c` on both sides (`git rev-parse
HEAD:<path>` vs `git hash-object <path>`, and an empty `git diff HEAD --
<path>`). Prose only, one file.
## The defect
Six subscribe/unsubscribe sites carried every subscription field at the
**top level**, in snake_case, where the declared contract nests them and
spells them camelCase. `SubscribeMessageSchema`
(`websocket.zod.ts:278`–`:281`) declares exactly `type: 'subscribe'`
plus a `subscription` object, on top of the three `BaseWebSocketMessage`
fields; `EventSubscriptionSchema` (`:102`–`:117`) declares
`subscriptionId` / `events` / `objects` / `filters` / `channels`. A
fence copied verbatim parses against no schema in the file.
The mismatch is **not one thing**, and each axis was verified
separately:
| page (top level) | declared | axes |
|:--|:--|:--|
| `subscription_id` | `subscription.subscriptionId` | nesting +
snake/camel + **`z.string().uuid()`, so `"sub_1"` does not parse** |
| `object`, a string | `subscription.objects`, an array | nesting +
singular/plural + scalar/array |
| `filter` | `subscription.filters` | nesting + singular/plural |
| `events: ["created", …]` | `z.array(EventPatternSchema)` | bare verbs
vs patterns — `DataEventType` publishes `data.record.created` |
| *absent* | `messageId`, `timestamp` | **both required by
`BaseWebSocketMessage`** — an axis not in the card |
### Two page keys map to nothing at all
`record_id` and `query` are declared **nowhere** in the protocol. The
narrowest scope the contract can express is object + event pattern, so
those two sections now say so and move the narrowing to the client
rather than showing fields no schema carries.
### Second, independent defect in the same bullet list
The page described `filter` as *"Optional filter (same syntax as HTTP
API filters)"*. The schema's docblock (`:106`–`:115`) says the opposite
in as many words — *"⚠️ **NOT YET ENFORCED** — no runtime evaluates a
payload filter. `matchesSubscription` matches on object name and event
type only … a subscription carrying `filters` receives every event its
patterns match."* The page promised enforcement the contract explicitly
disclaims; it now carries that disclaimer.
## Fence census — the round's first deliverable
Enumerated on this tree, **not** inherited from the dispatch's page-wide
counts.
| # | line (pre) | section | type | top-level keys |
|--:|--:|:--|:--|:--|
| 1 | 256–266 | Subscribe to Object Events | `subscribe` | `type`,
`subscription_id`, `object`, `events`, `filter` |
| 2 | 379–384 | **Unsubscribe** | `unsubscribe` | `type`,
`subscription_id` |
| 3 | 404–412 | Subscribe to Specific Record | `subscribe` | `type`,
`subscription_id`, `object`, `record_id`, `events` |
| 4 | 421–435 | Subscribe to Query Results | `subscribe` | `type`,
`subscription_id`, `object`, `query`, `events` |
| 5 | 655–662 | Permission Enforcement | `subscribe` | `type`,
`subscription_id`, `object`, `events` |
| 6 | 679–689 | **Client Reconnection (JS)** | `subscribe` | `type`,
`subscription_id` (+ `sub_${Date.now()}`) |
⭐ The card named **four** sections. The census found **six** sites:
**Unsubscribe** (#2) and the **JS client snippet** (#6) were not on the
card's list. #6 was also missed by a first pass that matched only JSON
`"type": "subscribe"` — it uses the single-quoted JS form, and the
census was widened to catch it.
After the edit all six carry exactly `messageId` / `type` / `timestamp`
/ `subscription` (or `request` for unsubscribe).
## Fork verdict: **(a) the page is wrong** — every fence
Producer evidence on this tree, all six fences landing on the same
verdict:
- `SubscribeMessageSchema`, `UnsubscribeMessageSchema` and
`EventSubscriptionSchema` have **zero** consumers outside
`packages/spec`'s own declaration, its unit test and the type-alias pin.
- `IRealtimeService.handleUpgrade` is deliberately unimplemented
platform-wide — `service-realtime/src/no-channel-route.pin.test.ts:49`
asserts it **is not a function**.
- `subscription_id` → **0** across **6120** tracked `packages/**` source
files (no `dist/`), with four lit controls on the same corpus and the
same call: `subscriptionId` **40**, `eventTypes` **27**, `handleUpgrade`
**18**, `EventSubscriptionSchema` **13**.
- The 660 `record_id` hits are the generic DB-column convention:
**zero** files carry both `record_id` and `WebSocket`.
Nothing produces or consumes a WebSocket subscribe message, so no
top-level field on one can be real. Reading (b) — the schema is
incomplete — would need a producer, and there is none.
**objectui: reported, not counted.** All WS-subscription probes return 0
there, but the schema-side control is also dark (`subscriptionId` 0,
`EventSubscription` 0) — only the generic `useState` (2262) lights,
which proves the corpus is real source and nothing more. A zero whose
control does not light is not a reading. `cloud` is not checked out
here: **NOT MEASURED**.
## Verification
- **40/40** derived gates green, on a clean full re-run after the
builds. Gate family derived mechanically via `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (it reads the change set from the merge base
itself), not hand-listed.
- Five gates first returned `PREREQUISITE NOT MET` / "build first" (exit
3 and exit 1) — **NOT MEASURED, not red**. After building
`@objectstack/lint...`, `@objectstack/spec` and
`@objectstack/client-react...` all five are green. Exit codes captured
**before** any pipe.
- MDX compiles: `pnpm exec fumadocs-mdx` exit 0.
- No generated or tracked artifact moved —
`authorable-surface.base.json` included. `git status --porcelain` shows
exactly one file.
- No control characters: `check:nul-bytes` exit 0, plus a direct `grep
-naP` sweep of the edited file.
## `skip-changeset`, and a conflict with the dispatch to flag
⚠️ **The dispatch asked for a patch changeset; this PR carries none, and
the label instead.** Reporting rather than silently choosing:
- `content/docs/**` ships in **no** published package's `files[]` — 0 of
70 publishable packages mention `content/` or `docs/` there, with all 70
shipping `dist` as the positive control.
- `@objectstack/docs`, which owns the page, is `private: true` — there
is no publishable package to bump. Naming any other package would ship a
release note for a package this PR does not touch.
- **Precedent on the identical surface**: #17700, the first half of this
very card, merged with no changeset.
By the standing criterion — nothing published moves — this is the
textbook `skip-changeset` case, which `lint.yml` states in those words.
Happy to add a changeset if the maintainer reads it the other way.
## Scope
Confined to the subscribe/unsubscribe fences and their Parameters bullet
lists. ⛔ Not `websocket.zod.ts`, ⛔ not the message-type sections #17700
corrected, ⛔ not `content/docs/releases/`, ⛔ no other page.
One finding deliberately left out and filed separately: the `type:
"event"` fences on this same page carry top-level `subscription_id` /
`event` / `data` / `changes` / `reason` where `EventMessageSchema`
declares `subscriptionId` / `eventName` / `payload` / `userId` and no
`changes` or `reason`. Same defect class, different message family — it
is not this round's declared face, and it is not silently widened into.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments